====== PHP RFC: Closure optimizations ====== * Date: 2026-01-30 * Author: Ilija Tovilo * Status: Under discussion * Proposed Version: PHP 8.next * Implementation: https://github.com/php/php-src/pull/19941 * RFC Discussion thread: https://news-web.php.net/php.internals/129957 * Original announcement thread: https://news-web.php.net/php.internals/129825 ===== Introduction ===== This RFC proposes two new optimizations for closures that come with some theoretical BC breaks. The purpose of this RFC is to evaluate whether these BC breaks are an acceptable trade-off for the gained performance. * Non-static closures are turned into static ones if they are guaranteed not to make use of ''$this''. * Stateless closures, i.e. those that are ''static'', don't capture any variables and don't declare any static variables, are cached between uses. ===== Static closure inference ===== This optimization will attempt to infer ''static'' for closures that are guaranteed not to make any use of ''$this''. class Foo { public $closure; public function __construct() { $this->closure = function() { echo "Hello world!"; }; } } Previously, the closure in ''__construct'' would have implicitly captured ''$this'', keeping the instance of ''Foo'' alive for the lifetime of the closure. Conversely, the instance of ''Foo'' would keep the closure alive, creating a reference cycle that requires running PHP's cycle collector to resolve. Frequently, such cycles are not resolved for the remainder of the request, given the cycle collector often doesn't run at all. These cycles can also make it more likely for the cycle collector to run in the first place, spending time on resolving a cycle that didn't need to exist in the first place. The aforementioned optimization will attempt to infer ''static'' for closures that fulfill the following (slightly esoteric) conditions. The closure must not: - use ''$this''. That's the obvious case. - use ''$$var'', given ''$var'' could refer to '''this'''. - use ''Foo::bar()'', given this could be a hidden instance call to a (grand-)parent method. - use ''$f()'', for the same reason as 3. - use ''call_user_func()'', for the same reason as 3. - declare another (uninferable) non-static closure, where ''$this'' flows from parent to child. - use ''require'', ''include'' or ''eval'', given the called code might do any of the above. These rules appear to be quite effective. A test was performed on Symfony Demo, where ''static'' modifiers were removed from all closures. The optimization was able to infer 68/87 (~78%) closures that were explicitly marked as ''static''. While explicit marking remains preferable, this optimization aims to benefit codebases that prefer not to add ''static'' to avoid visual clutter, as well as those who aren't aware of these subtle performance implications. ===== Stateless closure caching ===== Stateless closures, i.e. those that are ''static'', don't capture any variables and don't declare any static variables, are cached between uses. function test() { $x = static 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. With this second optimization, the first closure will be kept alive and cached for reuse. This small (and very synthetic) benchmark improves by ~80% on my machine. More practical improvements were also measured in the Laravel template, where these two optimizations can avoid 2384 out of 3637 closure instantiations, improving performance by ~3% on my machine. ===== Backward Incompatible Changes ===== There are three BC considerations. * ''ReflectionFunction::getClosureThis()'' will now return ''NULL'' for closures that are inferred as static. This might be slightly surprising, given ''static'' inference does not work 100% reliably, limited by the rules previously described. * Two stateless closures originating from the same lexical location will now be identical. I.e.: function test() { return function () {}; } test() === test(); // true * Objects that previously would have created cycles may be collected earlier, also triggering destructors earlier. While technically backward incompatible, this behavior is generally expected and more predictable. Of note is that ''Closure::bind()'' and ''Closure::bindTo()'' usually throw when attempting to bind an object to a static closure. In this RFC, passing an object to these methods is explicitly allowed and discarded only for closures that are inferred as static, but not those that are explicitly static. The aim is to retain backward compatibility when a closure can suddenly be inferred as static due to seemingly unrelated changes, such as removing a static method call. ===== Vote ===== Primary Vote requiring a 2/3 majority to accept the RFC: Voting opened on yyyy-mm-dd and closes on yyyy-mm-dd. * Yes * No * Abstain ===== References ===== * RFC Discussion thread: https://news-web.php.net/php.internals/129957 * Original announcement thread: https://news-web.php.net/php.internals/129825 * RFC Voting thread: TBD