作者:c9s

一直以来,我心中有一个疑问,就是 JIT compilation 为何一直难以在 Perl 或 PHP (Zend Engine) 这类 3P Language 中实现?继 LLVM 开源后,陆续听到许多 Language VM 尝试整合 LLVM 试验并得到很好的测试数据,却一直没有被整合到正式版本中,原因究竟为何?而 Lars Bak 带领团队开发的 V8 — JavaScript JIT Compiler 却在几年内直接成功达阵。

这个问题在我心中的答案逐渐明显了,主要原因在于这些 Language VM 在设计的时候,是以 Language Feature 导向来设计 Byte code。

为了要提高 Language VM 的效能,每组 byte code 都是被设计来做非常多的複杂的事情,甚至是複合式的工作,每个 opcode 的运算元 (Operand) 都不是固定型别,是动态型别,在执行期间才去检查型别、转换型别、接著才做运算,这和 Native Machine 执行指令预算的模式其实差异很大,在 x86 机器上,每个指令都有很明确的型别,而且每个指令都只做一件事情,分工分得很细,ADD 就是永远都是整数相加,不会突然给你一个浮点数硬要 ADD 可以直接处理,而是独立 FADD 出来做浮点运算,更不会突然给你一个物件要你转型。

以 PHP — Zend Engine 的设计来说,zend_op 的运算元不是纯量,而是 zval struct。这个 zval struct 是什麽呢?它可以是任何东西,可以是 Long integer、可以是 String、可以是 Double 可以是 Object,所以当运算子操作的时候,实际上是 ADD {zval} {zval} -> {zval}。

又以 Zend Engine 中的 Function Call 来说,每一个 Function Call 都会先调用 INIT_METHOD_CALL ,再安插多个 SEND_VAL 或 SEND_VAR 来传递参数,最后又执行 DO_FCALL,才完成整个调用,这样听起来好像不複杂,但其实每个 opcode 都需耗费千百道 x86 instruction 才能达成,Zend Engine 中连传递参数的 Call Frame 都比单纯的 x86 pop, push 複杂很多。若用 x86 来写的话,传递参数就是移动数值到暂存器,或只是几个 push instruction,回传结果也只要移动结果到 EAX 然后执行 RET 就完成了,自然就造成了数量级的效能差异。

由于 Zend Engine 的 Byte code 是这样设计,也因此就算是在 Runtime 做 JIT,使用 Byte code 为单位做编译,编译出来的执行代码也不会和 Zend Engine 中 — zend_vm_execute.h 所定义的 opcode handler 差上太多,因为该检查的还是要检查,该转型的还是要转型,且 zval 也没办法直接存放到暂存器 (Register) 中。

Zend Engine 中,有著数量相当庞大的 C 语言巨集 (Macro) 用以存取 zval 的数值、转换型别、执行期间取值等等操作,这些到了 JIT 中都要能够被编译成 Native Code,才有办法在 Runtime 运作。LLVM 的出现虽然让编译 C 语言巨集 (Macro) 变成容易做到,但所耗费的编译时间实在不适合拿来做 PHP 的 JIT,更不用说每一次 Request 进来就要重新编译。

虽然 libjit 可能处理大部分的 SSA, Register Allocation,甚至有自己的 IR,但是产生出来的代码不像 LLVM 那麽好,也不能编译巨集用以在 JIT 执行环境上执行。

此外,由于是 Garbage Collection based 的执行环境,变数会被回收,编译过的 Native Code 也不能直接绑定 zval 的记忆体位址,这也间接了增加执行期间的运算量。

如今,众所皆知的 Facebook HHVM 有办法做到 JIT compilation 其实最主要的原因,在于 HHBC (HHVM Byte Code) 设计得好,该 HHBC 的规格 针对每个细节都解释得很清楚,也比较低阶,接近于 Native code,因此在转译到 Native code ,相对的就比较容易得多。 Zend Engine III 在 Byte code 的设计上,由于一开始就没有想要做 JIT,再加上也没有针对需求定义清楚,更没有 Byte code 相关规格文件,也因此就很难转移整个 Runtime System 到 JIT 上。

2015 年二月,Dmitry Stogov 在信件论坛上释出了 PHP7 的前身,Zend JIT (原始码),主要采取 AOT (Ahead of time) compilation,后来没有成为 PHP7 改善效能的主要解决方案的原因,除了以上几点之外,还有因为 AOT 编译时期需要准确的型别资讯才有办法有效编译出 Native code,否则就得预先为每种参数型别都预先编译好一种版本的 Native code,这种做法得消耗掉大量的记忆体。

Lars Bak 可能在十年前就有想到这点,所以 V8 在设计之初,就是直接将 AST 编译成 native code 来执行,省去了 bytecode 转译成 native code 的时间,也省去了创建整个独立且虚拟的执行环境,不像现在的普遍的 Language VM 普遍采用 AST 编译成 bytecode,再转译成 native code (若有 JIT 的话)

此外 V8 所实现的 JIT 最强大的地方,在于高度反覆执行的代码上,已经编译好的 Native code,可以在执行期间反组译回来再做优化。

PHP 未来得实现快速且有效的 JIT,很可能还是得自行实作 Runtime 的 JIT 编译器,虽目前借助 LLVM 的力量能花费较少的功夫,但 LLVM 仍不够轻巧。 且由于 Zend Engine 为 Memory-based VM,Runtime 处理的东西更为複杂,若不依赖 LLVM,恐怕得等花上相当庞大的功夫才有办法完成。

Dmitry 在信件论坛上也表示:

“I’m not planning to invest into it in the near future. (PHP-7 takes all my time)”

针对 LLVM 的部分,Dmitry 也回应道:

“Right (意指 LLVM 编译速度较慢,不适合做 JIT). LLVM is not suitable for JIT. It’s a compiler without front-end part. We will probably go with DynASM from LuaJIT, Low Level Interpreter from WebKit or our own similar approach.”

HHVM Team 最常被问到的问题之一,就是为何不使用 LLVM 作为编译器后端?他们的看法也是一样:

One of the most common questions we get about HHVM is why we don’t use LLVM for code generation. The primary reason has always been that while LLVM is great at optimizing C, C++, Objective-C, and other similar statically-typed languages, PHP is dynamically typed. The kinds of optimizations that provide huge performance benefits for static languages tend to be less useful in dynamic languages, or at least overshadowed by all the dynamic dispatching that’s done based on runtime types. We knew that there was probably something to be gained from using LLVM as a backend, but there were many larger opportunities go after first.

简而言之,LLVM 对于静态型别语言的效果是非常好的,然而 PHP 是动态型别,静态型别语言的优化,其实对动态语言的优化并无太多助益。针对 GitHub 上的问题,Josh Watzman , HHVM 成员之一,也答覆道:

There are continuing experiments in using LLVM as part of the JITting process — basically, after we go through our bytecode and IR, instead of emitting x64 assembly, we emit LLVM bitcode, which LLVM can then optimize and generate x64. LLVM is pretty slow, so we’d only do this for the hottest of hot code.

PHP JIT 恐怕来日方长的原因,是整个 PHP 团队能够全天候投入在 Zend Engine 核心的人一隻手都数得出来,多数人急著提交的,多是新奇好玩的 Language Feature (internals 上其实花费了相当多唇舌在阻挡不好的 RFC)。再者,由于 JIT 工程非常浩大,又需要开发者无偿投入,最终结果也不见得会被合併上主干,不像 V8 是由 Google 出资,拼上一整组 VM 专家的火力,PHP 未来要完成 JIT 恐怕是难上加难。

虽是难上加难,却不失为一个好题目。目前看起来 Zend Engine 这个题目是比 Perl 简单得多。

余下全文(1/3)
分享这篇文章:

请关注我们:

发表评论

电子邮件地址不会被公开。 必填项已用*标注