Closure optimizations

From: Date: Wed, 21 Jan 2026 19:52:55 +0000
Subject: Closure optimizations
Groups: php.internals 
Request: Send a blank email to internals+get-129825@lists.php.net to get a copy of this message
Hi everyone I've created a PR implementing two new optimizations for closures. [1] There are some theoretical BC breaks, so please let me know if any of them are of concern to you. 1. Static inference The engine will attempt to infer static for closures that are guaranteed not to make any use of $this. This has the benefit of avoiding a cycle when a closure is stored in a property of the declarator object. Frequently, such objects and their closures will not be collected for the entirety of the request, given the GC often doesn't run at all. But the real reason for this optimization is to enable optimization nr. 2. It's worth noting the rules for inferring static are slightly esoteric. You can read more about the rules in the PR description. 2. Stateless closure caching Stateless closures, i.e. those that are static, don't capture any variables and don't declare any static variables are now cached. function test() { $x = function () {}; } for ($i = 0; $i < 10_000_000; $i++) { test(); } Previously, this would have created 10 000 000 closure instances, even though all closures are effectively identical. Now, the first closure will be kept alive for reuse. With these two optimizations, this small benchmark improves by ~80%. Of course, this is a very synthetic benchmark. However, I could also measure improvements in real applications that instantiate many stateless closures. For example, in the Laravel template these two optimizations can avoid 2384 out of 3637 closure instantiations, improving performance by ~3% on my machine. The foreshadowed BC breaks come down to a three things: 1. For closures that are inferred as static, ReflectionFunction::getClosureThis() will now return NULL. 2. Objects that would previously have created cycles may be collected earlier, also triggering destructors earlier. IMO, this is a feature and more predictable than the current behavior. 3. Two stateless closures originating from the same lexical location will now be identical. I.e.: function test() { return function () {}; } test() === test(); // true Of note is that Closure::bind() and Closure::bindTo() usually throw when attempting to bind an object to a static closure. In my PR, this is explicitly allowed only for closures that are inferred as static, but not those that are explicitly static. I'm looking forward to your feedback. If there are no concerns, I will merge this PR into master for PHP 8.6 in roughly two weeks. Ilija [1] https://github.com/php/php-src/pull/19941

« previous php.internals (#129825) next »