The Future of PHP — JIT Compilation

c9s
PHP Hacks
Published in
8 min readApr 16, 2016

一直以來,我心中有一個疑問,就是 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 簡單得多。

接下來會在 C9S 開源電台發布 PHP 核心探索系列文章,有興趣的朋友請點選追蹤。 XD

#PHP7 #PHP #ZendEngine #JIT #LLVM

--

--

c9s
PHP Hacks

Yo-an Lin, yet another programmer in 21 century