Zef 作者公开拆解了一次很典型、也很有启发的提速过程:一个极简 AST-walking 解释器,在不上 SSA、不做字节码、不碰机器码、没有 JIT、也不靠 GC 优化当主线的前提下,最终拿到 16.646 倍加速。

结果很直白。基线时,它比 CPython 3.10 慢 35.448 倍,比 Lua 5.4.7 慢近 80 倍,比 QuickJS-ng 0.14.0 慢 22.562 倍。优化后,仍约慢于 CPython 2.13 倍、Lua 4.781 倍、QuickJS 1.355 倍。没反超,但差距被砍掉了一大截。

这件事最值得看的,不是“又一个解释器跑快了”。而是它把一个常被说反的话题摆正了:很多动态语言性能问题,先不是输在没有 JIT,而是输在底层设计太粗。老子说“天下难事,必作于易”,放到运行时实现上,几乎就是白纸黑字的工程守则。

这 16.6 倍,主要是怎么来的

真正该记住的改动不多,4 类就够了。

  • 64 位 tagged value / NuN tagging:尽量让数字别动不动就堆分配。
  • 运算符直连.a + b 这样的操作,不再绕去做字符串分派和方法名查找。
  • 对象模型 + inline cache:这是主力项。作者给出的结果里,这一步单独就把整体拉到基线的 6.818 倍。
  • watchpoints 与哈希表、参数路径优化:继续压低 getter/setter、参数访问、对象查找这些热点成本。

这套东西听着不炫。也正因为不炫,才更说明问题。它没有靠什么编译器奇迹翻盘,而是把最常见、最该先修的慢路径一刀刀剁掉。

原文里反复出现的慢点,其实很多做解释器的人都见过:字符串键查找太多,std::unordered_map 到处顶着跑,作用域链递归下探,参数访问路径太绕,一些通用库抽象在热点里白白加成本。问题不神秘,就是底盘没打好。

这对两类人很具体。

一类是做脚本引擎、DSL、规则引擎、嵌入式运行时的人。如果你的团队还在 AST 直接解释阶段,这篇东西的意义很现实:别急着立项 JIT,先回去看值表示、对象访问、调用点缓存、变量查找路径。这里修不对,后面很难省。

另一类是做技术决策的人。要不要批一轮“上更重编译链”的预算,至少先问三个问题:热点里还有没有字符串分派?对象访问有没有 cache?参数和作用域查找是不是还在哈希表里反复打转?这三件事都没做,先谈 JIT,通常是在跳步骤。

真正值钱的,不是黑魔法,是把低级热路径清干净

我更在意的,是这篇稿把一个行业误区说得很具体:很多项目嘴上谈的是编译层神话,实际死的是解释层常识。

动态语言跑得慢,常见原因并不高深。对象字段每次都字符串查找,变量绑定层层递归查,数值运算频繁装箱拆箱,调用点没有缓存,方法分派路径又长又散。你要是把这些东西全留着,再去抱怨“没有 JIT 所以慢”,多少有点避重就轻。

这不是说 JIT 不重要。不是这个结论。更准确的说法是:很多项目离谈 JIT 还早,连解释器最热的那几条路都没铺平。

浏览器早年的引擎竞争,其实就有类似一幕。舞台上最容易被拿来讲故事的是 JIT,但真正把差距拉开的,常常是对象布局、隐藏类、内联缓存、GC 停顿控制这些脏活累活。Fred Brooks 那句“没有银弹”,放在这里一点不过时。今天很多团队把银弹两个字,直接换成了 JIT。

我不太买账的一种管理话术是:先把功能做完,性能以后用更高级的方案补。这话常常不假,但也常常害人。因为一旦值表示、对象模型、名字查找路径和调用约定从第一天就随手糊,后面补性能不是装修,是拆楼。

对工程团队来说,最直接的动作不是“马上换技术栈”,而是先做一次热路径盘点:

  • 统计解释器里最热的 10 个调用点
  • 区分时间到底花在分派、查找、装箱还是容器访问
  • 检查对象访问和函数调用是否已有稳定缓存
  • 把作用域链、参数绑定、哈希表访问从抽象层拉回到具体成本

如果这一步都没做,采购新编译基础设施、推动团队迁移,往往只会把问题包装得更贵。

该怎么看这份结果,边界在哪里

这篇稿子能说明很多事,但也不是万能证明。

先看边界。主线实现使用 Fil-C++ 起步,作者自己承认这大约带来 4 倍性能 handicap。文中确实还提到一个未完成的 Yolo-C++ 版本,出现了 66.9 倍提速,甚至在部分对比里领先。但那不是本文主线结果,不能和 16.646 倍这条线混写成“Zef 已全面超过 Lua、QuickJS、CPython”。目前不能这么说。

再看 benchmark。样本主要是 Richards、DeltaBlue、N-Body、Splay。它们很适合看解释器骨架、对象访问和调度成本,也足够支撑“基础设计会吞掉大量性能”这个判断。但它们不能直接推出所有真实工作负载都会得到同样收益。

真正还要观察的,至少有两件事。

第一,是更广的 workload。比如字符串处理更重的脚本、对象 churn 更频繁的业务负载、异常路径、模块加载、宿主交互。这些地方能不能保住收益,目前还看不清。

第二,是收益曲线会不会变陡。前面这 16.6 倍,很多来自把明显错误或粗糙设计修正。后面如果继续做字节码、GC、或者更成熟的原生编译链,当然可能再提速,但成本大概率会明显上升,回报未必还这么痛快。

所以,做运行时的人现在不该得出“JIT 不需要”的结论。更有用的结论是:

先把解释器底盘修到不荒唐,再决定要不要上更重的武器。

这对项目规划也有影响。如果你在评估自研脚本系统,或者考虑把某个 DSL 推到更大规模使用,这篇稿至少会让你把时间线改一改:先补对象模型和热点缓存,再决定是否延后字节码/JIT 方案;先测热路径,再谈迁移语言或重写运行时。很多预算,不该花在“更高级”的方案上,而该花在把基础路径做对。