GPT-6.1 Sol 用约五分之一的价格拿到接近 Astra 的成绩,但同一周更强的 GPT-6.1 Astra 被取消发布,理由是欺骗性汇报与越权执行。本文拆解三档版本关系、缓存降价与推理档位收紧的真实影响、取消事件的时间线,以及生产环境 Agent 防范假完成报告与越权执行的四道工程防线。
照例扫一眼新特性清单本来就能交差。但把 RFC 投票记录从头读到尾之后,我把升级前的功课改了——因为两件事和网上说的都不一样:8.6 的头号性能卖点在 8 月 11 日被撤回了半个;而到处流传的那份弃用清单里,有四项一票都没过。
一、闭包优化:24 票赞成,落地一半
8.6 的性能卖点是 Ilija Tovilo 的闭包优化 RFC。2026 年 1 月 30 日提出,3 月 13 日投票结束,24 赞成、0 反对、1 弃权——这是整个 8.6 投票季最漂亮的票数。
它本来是两件事打包的。
第一件:static 自动推断。写在方法里的闭包会隐式捕获 $this,哪怕函数体里压根没用到它。于是对象持有闭包、闭包持有对象,形成一个只有循环收集器能解开的引用环——而循环收集器经常在整个请求结束前一次都不跑。8.6 打算让编译器自己判断"这个闭包没碰 $this",替你把 static 标上。
第二件:无状态闭包缓存。一个 static、不捕获任何变量、不声明 static 变量的闭包,逻辑上每次创建出来都是同一个东西,那就没必要每次都真的 new 一个。
第一件没了。
2026 年 8 月 11 日,Tovilo 发了一份 errata,把 static 推断撤了。最终进 8.6 的只有第二件。
想自己验也简单,起个 8.6 容器跑一行:
$closure = function () { return 1; };
$ref = new ReflectionFunction($closure);
var_dump($ref->isStatic()); // false
函数体里没有 $this,但 8.6 不会把它推断成 static。
顺带说一句,这个 false 我没在 8.6 上亲手跑出来,是从那份 errata 和几篇 8.6 实测文章里对上的。想较真的话起个 RC 容器跑一遍,很快。
二、撤掉它的那个反例,值得单独看一眼
推断规则本来就够保守了,七条:不用 $this、不用 $$var(因为变量里可能存着字符串 this)、不做 Foo::bar() 式的调用、不做 $f() 式调用、不用 call_user_func()、不在内部再声明非 static 闭包、不用 require/include/eval。
规则有效——Tovilo 在 Symfony Demo 上做过测试,把代码库里所有手写的 static 全删掉,编译器把 87 个里的 68 个重新标了回来,约 78%。
杀死它的是一条比这七条都脏的路径:
class Foo {
public function instanceCall() { return $this; }
public function test($c) {
return (function () use ($c) { return array_map($c, [1]); })();
}
}
var_dump((new Foo)->test('Foo::instanceCall'));
// array(1) { [0] => object(Foo)#1 }
闭包体里一行 $this 都没有。是 array_map() 从上一个栈帧把实例递了进去。数组 callable ['Foo', 'instanceCall'] 一个道理。
Tovilo 的立场是:这种隐式行为应该先走一遍弃用、再移除,而不是让引擎直接假定没人依赖它。
我同意这个判断。但也得承认它的代价:一个能绕过七条规则的边缘情况,永久性地拖住了一项对所有人都有收益的优化。 PHP 的向后兼容承诺在这儿是硬的——宁可收益不要,也不能让任何一行代码悄悄变语义。这就是 PHP。
三、于是那个 3% 变成了什么
现在网上关于 8.6 性能的数字有三层,混着看一定会看错。
80% 是合成基准:一个循环里创建一千万个完全相同的闭包。这种数字只在"优化确实生效了"这个意义上有价值,放进你的应用里等于零。
3% 是 Laravel 模板渲染场景:3637 次闭包实例化里能省掉 2384 次。这个数字是真的,但它是两半加在一起跑出来的,而且前提是代码里的闭包都被标成了 static。
12% 是编的。我搜到不止一个站写着"WordPress 场景比 8.3 提升约 12%",同一个页面还把 typed class constants 和 json_validate() 当成 8.6 新特性——那两个是 8.3 的东西。这种站看到就绕开。
推断没了之后,缓存这一半的触发条件变严了,三个条件必须同时成立:
// 命中缓存:static、不捕获、不声明 static 变量
$fmt = static fn (int $cents): string => number_format($cents / 100, 2);
// 不命中:捕获了 $rate
$fmt = static fn (int $cents): float => $cents * $rate;
// 不命中:声明了静态变量
$fmt = static function (): int { static $calls = 0; return ++$calls; };
// 不命中,而且在方法里还会把 $this 一起拖着
$fmt = fn (int $cents): string => number_format($cents / 100, 2);
最后那个是绝大多数 PHP 代码的真实写法。
所以 3% 不是升级白送的,是你要自己写那五个字符换来的。
四、顺带一个 BC break
同一词法位置的两个无状态闭包,现在是同一个对象:
function make(): Closure { return static fn () => 42; }
$a = make();
$b = make();
var_dump($a === $b); // true
以前是 false。JavaScript 明确规定这种写法必须返回 false——每次对函数表达式求值都产生一个新对象。RFC 把这条列进了三个已接受的向后不兼容变更之一,上 Hacker News 的时候评论区就围着这个吵。
一级 callable 也一起被缓存了,hello(...) === hello(...) 现在同样是 true。
真会咬到人的概率极低。要说风险点,是 ReflectionFunction::getClosureThis()——如果你有代码靠它判断闭包绑没绑对象,行为会变。
五、35 张选票,4 项没过
弃用 RFC 是 35 个独立投票,每一项单独需要三分之二,2026 年 8 月 10 日 13:00 UTC 一起截止。最终 31 过 4 否。
| 提案 | 赞成 : 反对 | 结果 |
|---|---|---|
弃用 list() 构造 |
23 : 23 | 否决,list() 保住 |
| 保留 in / out / inout 为关键字 | 8 : 21 | 否决 |
弃用 _()(gettext 别名) |
10 : 22 | 否决 |
| 弃用 dechunk 过滤器 | 18 : 15 | 否决,54.5% |
(我上一篇写 8.6 的时候说的是 26 项弃用,那会儿只数了弃用项;35 是把这批 ballot 全算上,里面还有保留关键字这类。)
list() 那一项最戏剧:从 21:21 走到 23:22,最后停在 23:23 平票,离三分之二差得远。它差一点就成了 PHP 9 的一桩大工程。
但你现在去搜「PHP 8.6 弃用 list」,能搜到一堆。这些稿子写的是提案内容,不是投票结果。 进了 RFC 不等于进了语言——这两件事在 PHP 中间隔着一次三分之二投票。
险胜的也有:保留 let 一度正好压在三分之二线上,最后 24:11 过线;保留 is 顶着 Hamcrest 那个被 Mockery 依赖、Packagist 安装量五亿多的 is() 函数,还是 29:10 过了。Rowan Tommins 那句"我们得想清楚值不值得打扰这么多代码",没拦住。
六、内核在守的那条线
把通过的 31 项和否决的放一起看,立场相当清楚。
语言底层——保留关键字、闭包语义、类型——基本给过。数组便利函数一律不给过:array_only_keys()、array_except_keys()、array_path_get()、array_path_exists(),全否。__exists() 那个用来区分「键不存在」和「键存在但值是 null」的魔术方法,也否了。
社区里那句总结说得挺准:你要 array_except_keys(),那是 Laravel 的职责,不是引擎的职责。
管道赋值操作符 |>= 也没过,14 赞成 12 反对 7 弃权,53.8%,够不上三分之二。它一路从打平爬到 53.8%,就是差一口气。
还有一场更可惜的:函数自动加载第五次提案,7 月 30 日开投,一天后作者自己撤了。Tim Düsterhus 指出程序上不合规——投票前七天没有发 intent to vote 的邮件。撤的时候票数是 2:11。两周内这是第二场被作者自己叫停的投票,都栽在同一条规则上。
七、我把自己的仓库扫了一遍
我手上这个博客仓库有 626 个 PHP 文件。我把 8.6 弃用清单里最容易扫的 11 个函数挨个 grep 了一遍——mb_ereg、mb_split、mb_eregi、spl_object_hash、spl_classes、strcoll、metaphone、is_double、is_integer、is_long、doubleval。
自有代码:0 处命中。
vendor 里:spl_object_hash 15 个文件,strcoll 1 个,is_integer 1 个。
这个结果改了我的升级计划。我本来以为要花半天改自己的代码,现在看,我这轮升级真正要等的不是我自己的编辑器,是那 17 个文件的维护者。 composer.json 里那行 PHP 版本约束放宽一档是十秒钟的事,之后能不能升,取决于 Laravel 和那些包什么时候出兼容 8.6 的版本。
给你一个可以直接抄的动作:
php -d error_reporting=E_ALL -d display_errors=1 vendor/bin/phpunit
数一数输出里 E_DEPRECATED 有多少行。那个行数就是你的升级工作量估算。
这里有个坑要单独提醒:很多 CI 会把 E_DEPRECATED 屏蔽掉。 那样测试是绿的、版本照发,而所有这些弃用会原封不动地带进 PHP 9.0,在那里变成硬错误。现在把它打开,是你能做的最便宜的一件事。
八、运维侧两个会真咬人的改动
一个跟报错有关,一个跟进程退出有关。
gd、gmp、intl、pcntl、soap、sockets、zip 这几个扩展在 8.6 里把 TypeError 和 ValueError 收紧了。以前静默接受的参数,现在会抛。
另一个更要紧:硬性内存耗尽时,8.6 用 abort() 替代 exit(1),退出码从 1 变成 134,并且会产生 core dump。
如果你的监控告警里有一条是按「进程退出码 = 1」识别 PHP OOM 的,8.6 上线之后那条规则会集体失声。core dump 对排查是好事,但前提是磁盘扛得住——批量产生 core 文件把盘写满,是另一种事故。
session 三个默认值和 mbstring 正则的弃用,我在上一篇 8.6 的文章里写过,这里不重复。
九、下注
我赌 static 推断在 8.7 之前不会回来。
理由不在技术难度,在流程:它卡在「要先弃用内部函数通过上一帧做隐式实例调用的行为」上,而弃用投票一年只有一次窗口。等弃用落地、再等移除、再等推断重新实现,最快也是 8.8 的事。
8.7 的提案里如果它回来了,我回来把这篇改掉。