Skip to content

Allow Closures to reuse their run_time_cache when opcache is enabled #18556

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

arnaud-lb
Copy link
Member

Possible alternative fix for GH-10782

This implements the approach described in #10782 (comment):

may be storing "scope" in the first slot of the closure's run_time_cache

Unfortunately this results in a small regression on the Symfony benchmark (0.20% wall time, 0.03% icount).

Copy link
Member

@bwoebi bwoebi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not only is it a CPU time regression, but it also leaks memory on the arena (it's just not reported as a leak, because arena).
Every time the scope changes (e.g. an inherited function called on different extending objects), a new run time cache is allocated.

If you want to meaningfully do this, you'll have to store the closures run_time_cache in e.g. class_mutable_data of the scope they belong to.

Otherwise, I think Dmitrys suggestion was to do this just once. I.e. store the scope for the first time the runtime cache is allocated (i.e. when ZEND_MAP_PTR_GET() is NULL) and otherwise heap allocate.
This would still cause repeated observer initializations, but make it consistent between opcache and non-opcache.

@arnaud-lb
Copy link
Member Author

arnaud-lb commented May 14, 2025

Yeah, I planned to analyze the regression and check for mistakes, because I didn't expected that.

My goal was only to have a similar reuse rate with opcache than without, but indeed there may be ways to improve that even more.

it also leaks memory on the arena [...]. Every time the scope changes (e.g. an inherited function called on different extending objects), a new run time cache is allocated.

I'm not sure what you mean here. Do you have an example where the scope of a closure changes? (without binding it explicitly). All I can think of is traits, but even in that case I allocate on the arena only once.

With the current changes, I never allocate on the arena more than once per Closure.

Otherwise, I think Dmitrys suggestion was to do this just once. I.e. store the scope for the first time the runtime cache is allocated (i.e. when ZEND_MAP_PTR_GET() is NULL) and otherwise heap allocate.

That's what I was targeting, but it's possible I've made mistakes that prevent this.

@bwoebi
Copy link
Member

bwoebi commented May 14, 2025

			} else if (!ptr
			 && func->common.fn_flags & ZEND_ACC_CLOSURE) {

I see what I missed - the !ptr and the closure check are on two different lines and missed that they belong to the same condition.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants