PHP 8.6 闭包优化只合了一半:无状态闭包缓存已落地,static 推断被撤回
PHP 8.6 的 Closure optimizations RFC(作者 Ilija Tovilo,2026-01-30,状态 Partially implemented)原本提出两个闭包(含箭头函数)优化,附带理论上的 BC break。投票于 2026-02-27 开启、2026-03-13 关闭,要求 2/3 多数。最终只合并了其中一个:无状态闭包缓存。另一个“非 static 闭包自动推断为 static”因边缘情况被撤回。
未落地的 static 推断
原优化一是:无法推断使用 $this 的非 static 闭包,自动变成 static。判断条件偏 esoteric,闭包需要不满足以下任一条件:
- 不使用
$this; - 不使用
$$var($var可能指向$this); - 不使用
Foo::bar()(可能是对父/祖父方法的隐式实例调用); - 不使用
$f()(同因); - 不使用
call_user_func()(同因); - 不在内部声明另一个(无法推断的)非 static 闭包(
$this从父流向子); - 不使用
require/include/eval(被调代码可能做上述任何事)。
2026-08-11 errata 说明了撤回原因:投票结束后发现边缘情况。内部函数 array_map() 可以仅凭一个命名 callable 通过前一个栈帧做实例调用;许多其他内部调用以及 array callable(['Foo','instanceCall'])同理。这种 esoteric 行为应先被弃用再移除,不能直接假定没人用。因此只合并了 RFC 中的“无状态闭包缓存”部分。所有相关函数都被正确标注 static 的代码才能拿到全部性能收益。
验证 static 推断未生效:
$closure = function () {
return 1;
};
$ref = new ReflectionFunction($closure);
var_dump($ref->isStatic()); // false
这个闭包不使用 $this,但 PHP 8.6 不会把它推断成 static。
已落地的无状态闭包缓存
原优化二是:无状态闭包(static、不捕获变量、不声明静态变量)在使用之间缓存复用。此前即使循环里生成一百万个逻辑相同的闭包,也会创建一百万个实例;优化后第一个闭包被保留并复用。
合成基准提升约 80%;更实际的 Laravel 模板测试中,3637 次闭包实例化里有 2384 次可被省掉,性能提升约 3%。
同一词法位置、static、不捕获变量的闭包现在复用同一实例,可以用 === 判等:
function make(): Closure
{
return static fn () => 42;
}
$a = make();
$b = make();
var_dump($a === $b); // true
如果闭包捕获变量,则不会进入缓存复用:
function makeWithCapture(int $x): Closure
{
return static fn () => $x;
}
$c = makeWithCapture(1);
$d = makeWithCapture(1);
var_dump($c === $d); // false
第一类可调用缓存
另据 PHP 8.6 NEWS:Core 新增 first-class callable cache,在请求周期内共享实例。
function hello(): void {}
$a = hello(...);
$b = hello(...);
var_dump($a === $b); // true
BC 影响
提案中列出的 Backward Incompatible Changes 包括:
ReflectionFunction::getClosureThis()对被推断为 static 的闭包将返回NULL(因 static 推断并非 100% 可靠,可能让人意外)。- 来自同一词法位置的两个无状态闭包现在会判定为相同(identical)。
Closure::bind()/bindTo()通常对 static 闭包抛异常;本 RFC 中,对“被推断为 static”(而非显式 static)的闭包,传入对象会被显式允许并丢弃,以保留向后兼容。
由于最终只合并了“无状态闭包缓存”,与 static 推断相关的 BC 未随本次合并生效;实际落地的主要是同一词法位置的无状态闭包会被判定为相同。
要在 PHP 8.6 上拿到全部性能收益,需要显式给闭包加 static。