HHVM 是如何提升 PHP 性能的[轉(zhuǎn)]
Jun 06, 2016 pm 08:14 PM背景 HHVM 是 Facebook 開發(fā)的高性能 PHP 虛擬機(jī),宣稱比官方的快9倍,我很好奇,于是抽空簡(jiǎn)單了解了一下,并整理出這篇文章,希望能回答清楚兩方面的問題 HHVM 到底靠譜么?是否可以用到產(chǎn)品中? 它為什么比官方的 PHP 快很多?到底是如何優(yōu)化的? 你會(huì)怎么
背景
HHVM 是 Facebook 開發(fā)的高性能 PHP 虛擬機(jī),宣稱比官方的快9倍,我很好奇,于是抽空簡(jiǎn)單了解了一下,并整理出這篇文章,希望能回答清楚兩方面的問題
- HHVM 到底靠譜么?是否可以用到產(chǎn)品中?
- 它為什么比官方的 PHP 快很多?到底是如何優(yōu)化的?
你會(huì)怎么做?
在討論 HHVM 實(shí)現(xiàn)原理前,我們先設(shè)身處地想想:假設(shè)你有個(gè) PHP 寫的網(wǎng)站遇到了性能問題,經(jīng)分析后發(fā)現(xiàn)很大一部分資源就耗在 PHP 上,這時(shí)你會(huì)怎么優(yōu)化 PHP 性能?
比如可以有以下幾種方式:
- 方案1,遷移到性能更好的語言上,如 Java、C++、Go。
- 方案2,通過 RPC 將功能分離出來用其它語言實(shí)現(xiàn),讓 PHP 做更少的事情,比如 Twitter 就將大量業(yè)務(wù)邏輯放到了 Scala 中,前端的 Rails 只負(fù)責(zé)展現(xiàn)。
- 方案3,寫 PHP 擴(kuò)展,在性能瓶頸地方換 C/C++。
- 方案4,優(yōu)化 PHP 的性能
方案1幾乎不可行,十年前 Joel 就拿Netscape的例子警告過,你將放棄是多年的經(jīng)驗(yàn)積累,尤其是像 Facebook 這種業(yè)務(wù)邏輯復(fù)雜的產(chǎn)品,PHP 代碼實(shí)在太多了,據(jù)稱有2千萬行(引用自 [PHP on the Metal with HHVM]),修改起來的成本恐怕比寫個(gè)虛擬機(jī)還大,而且對(duì)于一個(gè)上千人的團(tuán)隊(duì),從頭開始學(xué)習(xí)也是不可接受的。
方案2是最保險(xiǎn)的方案,可以逐步遷移,事實(shí)上 Facebook 也在朝這方面努力了,而且還開發(fā)了 Thrift 這樣的 RPC 解決方案,F(xiàn)acebook 內(nèi)部主要使用的另一個(gè)語言是 C++,從早期的 Thrift 代碼就能看出來,因?yàn)槠渌Z言的實(shí)現(xiàn)都很簡(jiǎn)陋,沒法在生產(chǎn)環(huán)境下使用。
目前在 Facebook 中據(jù)稱 PHP:C++ 已經(jīng)從9:1增加到7:3了,加上有 Andrei Alexandrescu 的存在,C++ 在 Facebook 中越來越流行,但這只能解決部分問題,畢竟 C++ 開發(fā)成本比 PHP 高得多,不適合用在經(jīng)常修改的地方,而且太多 RPC 的調(diào)用也會(huì)嚴(yán)重影響性能。
方案3看起來美好,實(shí)際執(zhí)行起來卻很難,一般來說性能瓶頸并不會(huì)很顯著,大多是不斷累加的結(jié)果,加上 PHP 擴(kuò)展開發(fā)成本高,這種方案一般只用在公共且變化不大的基礎(chǔ)庫上,所以這種方案解決不了多少問題。
可以看到,前面3個(gè)方案并不能很好地解決問題,所以 Facebook 其實(shí)沒有選擇的余地,只能去考慮 PHP 本身的優(yōu)化了。
更快的 PHP
既然要優(yōu)化 PHP,那如何去優(yōu)化呢?在我看來可以有以下幾種方法
- 方案1,PHP 語言層面的優(yōu)化。
- 方案2,優(yōu)化 PHP 的官方實(shí)現(xiàn)(也就是 Zend)。
- 方案3,將 PHP 編譯成其它語言的 bytecode(字節(jié)碼),借助其它語言的虛擬機(jī)(如 JVM)來運(yùn)行。
- 方案4,將 PHP 轉(zhuǎn)成 C/C++,然后編譯成本地代碼。
- 方案5,開發(fā)更快的 PHP 虛擬機(jī)。
PHP 語言層面的優(yōu)化是最簡(jiǎn)單可行的,F(xiàn)acebook 當(dāng)然想到了,而且還開發(fā)了XHProf這樣的性能分析工具,對(duì)于定位性能瓶頸是很有幫助的。
不過 XHProf 還是沒能很好解決 Facebook 的問題,所以我們繼續(xù)看,接下來是方案2,簡(jiǎn)單來看,Zend 的執(zhí)行過程可以分為兩部分:將 PHP 編譯為 opcode、執(zhí)行 opcode,所以優(yōu)化 Zend 可以從這兩方面來考慮。
優(yōu)化 opcode 是一種常見的做法,可以避免重復(fù)解析 PHP,而且還能做一些靜態(tài)的編譯優(yōu)化,比如Zend Optimizer Plus,但由于 PHP 語言的動(dòng)態(tài)性,這種優(yōu)化方法是有局限性的,樂觀估計(jì)也只能提升20%的性能。另一種考慮是優(yōu)化 opcode 架構(gòu)本身,如基于寄存器的方式,但這種做法修改起來工作量太大,性能提升也不會(huì)特別明顯(可能30%?),所以投入產(chǎn)出比不高。
另一個(gè)方法是優(yōu)化 opcode 的執(zhí)行,首先簡(jiǎn)單提一下 Zend 是如何執(zhí)行的,Zend 的 interpreter(也叫解釋器)在讀到 opcode 后,會(huì)根據(jù)不同的 opcode 調(diào)用不同函數(shù)(其實(shí)有些是 switch,不過為了描述方便我簡(jiǎn)化了),然后在這個(gè)函數(shù)中執(zhí)行各種語言相關(guān)的操作(感興趣的話可看看深入理解PHP內(nèi)核這本書),所以 Zend 中并沒有什么復(fù)雜封裝和間接調(diào)用,作為一個(gè)解釋器來說已經(jīng)做得很好了。
想要提升 Zend 的執(zhí)行性能,就需要對(duì)程序的底層執(zhí)行有所解,比如函數(shù)調(diào)用其實(shí)是有開銷的,所以能通過 Inline threading 來優(yōu)化掉,它的原理就像 C 語言中的 inline 關(guān)鍵字那樣,但它是在運(yùn)行時(shí)將相關(guān)的函數(shù)展開,然后依次執(zhí)行(只是打個(gè)比方,實(shí)際實(shí)現(xiàn)不太一樣),同時(shí)還避免了 CPU 流水線預(yù)測(cè)失敗導(dǎo)致的浪費(fèi)。
另外還可以像 JavaScriptCore 和 LuaJIT 那樣使用匯編來實(shí)現(xiàn) interpreter,具體細(xì)節(jié)建議看看 Mike的解釋
但這兩種做法修改代價(jià)太大,甚至比重寫一個(gè)還難,尤其是要保證向下兼容,后面提到 PHP 的特點(diǎn)時(shí)你就知道了。
開發(fā)一個(gè)高性能的虛擬機(jī)不是件簡(jiǎn)單的事情,JVM 花了10多年才達(dá)到現(xiàn)在的性能,那是否能直接利用這些高性能的虛擬機(jī)來優(yōu)化 PHP 的性能呢?這就是方案3的思路。
其實(shí)這種方案早就有人嘗試過了,比如 Quercus 和 IBM 的 P8,Quercus 幾乎沒見有人使用,而 P8 也已經(jīng)死掉了。Facebook 也曾經(jīng)調(diào)研過這種方式,甚至還出現(xiàn)過不靠譜的傳聞 ,但其實(shí) Facebook 在2011年就放棄了。
因?yàn)榉桨?看起來美好,但實(shí)際效果卻不理想,按照很多大牛的說法(比如 Mike),VM 總是為某個(gè)語言優(yōu)化的,其它語言在上面實(shí)現(xiàn)會(huì)遇到很多瓶頸,比如動(dòng)態(tài)的方法調(diào)用,關(guān)于這點(diǎn)在 Dart的文檔中有過介紹,而且據(jù)說 Quercus 的性能與 Zend+APC 比差不了太多([來自The HipHop Compiler for PHP]),所以沒太大意義。
不過 OpenJDK 這幾年也在努力,最近的 Grall 項(xiàng)目看起來還不錯(cuò),也有語言在上面取得了顯著的效果,但我還沒空研究 Grall,所以這里無法判斷。
接下來是方案4,它正是 HPHPc(HHVM 的前身)的做法,原理是將 PHP 代碼轉(zhuǎn)成 C++,然后編譯為本地文件,可以認(rèn)為是一種 AOT(ahead of time)的方式,關(guān)于其中代碼轉(zhuǎn)換的技術(shù)細(xì)節(jié)可以參考 The HipHop Compiler for PHP這篇論文,以下是該論文中的一個(gè)截圖,可以通過它來大概了解:
這種做法的最大優(yōu)點(diǎn)是實(shí)現(xiàn)簡(jiǎn)單(相對(duì)于一個(gè) VM 來說),而且能做很多編譯優(yōu)化(因?yàn)槭请x線的,慢點(diǎn)也沒事),比如上面的例子就將- 1
優(yōu)化掉了,但它很難支持 PHP 中的很多動(dòng)態(tài)的方法,如 eval()
、create_function()
,因?yàn)檫@就得再內(nèi)嵌一個(gè) interpreter,成本不小,所以 HPHPc 干脆就直接不支持這些語法。
除了 HPHPc,還有兩個(gè)類似的項(xiàng)目,一個(gè)是 Roadsend,另一個(gè)是 phc ,phc 的做法是將 PHP 轉(zhuǎn)成了 C 再編譯,以下是它將 file_get_contents($f)
轉(zhuǎn)成 C 代碼的例子
<code>static php_fcall_info fgc_info; php_fcall_info_init ("file_get_contents", &fgc_info); php_hash_find (LOCAL_ST, "f", 5863275, &fgc_info.params); php_call_function (&fgc_info); </code>
話說 phc作者曾經(jīng)在博客上哭訴,說他兩年前就去 Facebook 演示過 phc 了,還和那里的工程師交流過,結(jié)果人家一發(fā)布就火了,而自己忙活了4年卻默默無聞,現(xiàn)在前途渺茫。。。
Roadsend 也已經(jīng)不維護(hù)了,對(duì)于 PHP 這樣的動(dòng)態(tài)語言來說,這種做法有很多的局限性,由于無法動(dòng)態(tài) include,F(xiàn)acebook 將所有文件都編譯到了一起,上線時(shí)的文件部署居然達(dá)到了 1G,越來越不可接受了。
另外有還有一個(gè)叫 PHP QB 的項(xiàng)目,由于時(shí)間關(guān)系我沒有看,感覺可能是類似的東東。
所以就只剩下一條路了,那就是寫一個(gè)更快的 PHP 虛擬機(jī),將一條黑路走到底,或許你和我一樣,一開始聽到 Facebook 要做一個(gè)虛擬機(jī)是覺得太離譜,但如果仔細(xì)分析就會(huì)發(fā)現(xiàn)其實(shí)也只有這樣了。
更快的虛擬機(jī)
HHVM 為什么更快?在各種新聞報(bào)道中都提到了 JIT 這個(gè)關(guān)鍵技術(shù),但其實(shí)遠(yuǎn)沒有那么簡(jiǎn)單,JIT 不是什么神奇的魔法棒,用它輕輕一揮就能提升性能,而且 JIT 這個(gè)操作本身也是會(huì)耗時(shí)的,對(duì)于簡(jiǎn)單的程序沒準(zhǔn)還比 interpreter 慢,最極端的例子是 LuaJIT 2 的 Interpreter 就稍微比 V8 的 JIT 快,所以并不存在絕對(duì)的事情,更多還是在細(xì)節(jié)問題的處理上,HHVM 的發(fā)展歷史就是不斷優(yōu)化的歷史,你可以從下圖看到它是如何一點(diǎn)點(diǎn)超過 HPHPc 的:
值得一提的是在 Android 4.4 中新的虛擬機(jī) ART 就采用的是 AOT 方案(還記得么?前面提到的 HPHPc 就是這種),結(jié)果比之前使用 JIT 的 Dalvik 快了一倍,所以說 JIT 也不一定比 AOT 快。
因此這個(gè)項(xiàng)目是有很大風(fēng)險(xiǎn)的,如果沒有強(qiáng)大的內(nèi)心和毅力,極有可能半途而廢,Google就曾經(jīng)想用 JIT 提升Python 的性能,但最終失敗了,對(duì)于 Google 來說用到 Python 的地方其實(shí)并沒什么性能問題(好吧,以前 Google 是用 Python 寫過 crawl [參考 In The Plex],但那都是1996年的事情了)。
比起 Google,F(xiàn)acebook 顯然有更大的動(dòng)力和決心,PHP 是 Facebook 最重要的語言,我們來看看 Facebook 都投入了哪些大牛到這個(gè)項(xiàng)目中(不全)
- Andrei Alexandrescu,『Modern C++ Design』和『C++ Coding Standards』的作者,C++ 領(lǐng)域無可爭(zhēng)議的大神
- Keith Adams,負(fù)責(zé)過 VMware 核心架構(gòu),當(dāng)年 VMware 就派他一人去和 Intel 進(jìn)行技術(shù)合作,足以證明在 VMM 領(lǐng)域他有多了解了
- Drew Paroski,在微軟參與過 .NET 虛擬機(jī)開發(fā),改進(jìn)了其中的 JIT
- Jason Evans,開發(fā)了 jemalloc,減少了 Firefox 一半的內(nèi)存消耗
- Sara Golemon,『Extending and Embedding PHP』的作者,PHP 內(nèi)核專家,這本書估計(jì)所有 PHP 高手都看過吧,或許你不知道其實(shí)她是女的
雖然沒有像 Lars Bak、Mike Pall 這樣在虛擬機(jī)領(lǐng)域的頂級(jí)專家,但如果這些大牛能齊心協(xié)力,寫個(gè)虛擬機(jī)還是問題不大的,那么他們將面臨什么樣的挑戰(zhàn)呢?接下來我們一一討論。
規(guī)范是什么?
自己寫 PHP 虛擬機(jī)要面臨的第一個(gè)問題就是 PHP 沒有語言規(guī)范,很多版本間的語法還會(huì)不兼容(甚至是小版本號(hào),比如 5.2.1 和 5.2.3),PHP 語言規(guī)范究竟如何定義呢?來看一篇來自 IEEE 的說法:
The PHP group claim that they have the ?nal say in the speci?cation of (the language) PHP. This groups speci?cation is an implementation, and there is no prose speci?cation or agreed validation suite.
所以唯一的途徑就是老老實(shí)實(shí)去看 Zend 的實(shí)現(xiàn),好在 HPHPc 中已經(jīng)痛苦過一次了,所以 HHVM 能直接利用現(xiàn)成,因此這個(gè)問題并不算太大。
語言還是擴(kuò)展?
實(shí)現(xiàn) PHP 語言不僅僅只是實(shí)現(xiàn)一個(gè)虛擬機(jī)那么簡(jiǎn)單,PHP 語言本身還包括了各種擴(kuò)展,這些擴(kuò)展和語言是一體的,Zend 不辭辛勞地實(shí)現(xiàn)了各種你可能會(huì)用到的功能。如果分析過 PHP 的代碼,就會(huì)發(fā)現(xiàn)它的 C 代碼除去空行注釋后居然還有80+萬行,而你猜其中 Zend 引擎部分有多少?只有不到10萬行。
對(duì)于開發(fā)者來說這不是什么壞事,但對(duì)于引擎實(shí)現(xiàn)者來說就很悲劇了,我們可以拿 Java 來進(jìn)行對(duì)比,寫個(gè) Java 的虛擬機(jī)只需實(shí)現(xiàn)字節(jié)碼解釋及一些基礎(chǔ)的 JNI 調(diào)用,Java 絕大部分內(nèi)置庫都是用 Java 實(shí)現(xiàn)的,所以如果不考慮性能優(yōu)化,單從工作量看,實(shí)現(xiàn) PHP 虛擬機(jī)比 JVM 要難得多,比如就有人用8千行的 TypeScript 實(shí)現(xiàn)了一個(gè) JVM Doppio。
而對(duì)于這個(gè)問題,HHVM 的解決辦法很簡(jiǎn)單,那就是只實(shí)現(xiàn) Facebook 中用到的,而且同樣可以先用 HPHPc 中之前寫過的,所以問題也不大。
實(shí)現(xiàn) Interpreter
接下來是 Interpreter 的實(shí)現(xiàn),在解析完 PHP 后會(huì)生成 HHVM 自己設(shè)計(jì)的一種 Bytecode,存儲(chǔ)在~/.hhvm.hhbc
(SQLite 文件) 中以便重用,在執(zhí)行 Bytecode 時(shí)和 Zend 類似,也是將不同的字節(jié)碼放到不同的函數(shù)中去實(shí)現(xiàn)(這種方式在虛擬機(jī)中有個(gè)專門的稱呼:Subroutine threading)
Interpreter 的主體實(shí)現(xiàn)在 bytecode.cpp 中,比如 VMExecutionContext::iopAdd
這樣的方法,最終執(zhí)行會(huì)根據(jù)不同類型來區(qū)分,比如 add 操作的實(shí)現(xiàn)是在 tv-arith.cpp 中,下面摘抄其中的一小段
<code>if (c2.m_type == KindOfInt64) return o(c1.m_data.num, c2.m_data.num); if (c2.m_type == KindOfDouble) return o(c1.m_data.num, c2.m_data.dbl); </code>
正是因?yàn)橛辛?Interpreter,HHVM 在對(duì)于 PHP 語法的支持上比 HPHPc 有明顯改進(jìn),理論上做到完全兼容官方 PHP,但僅這么做在性能并不會(huì)比 Zend 好多少,由于無法確定變量類型,所以需要加上類似上面的條件判斷語句,但這樣的代碼不利于現(xiàn)代 CPU 的執(zhí)行優(yōu)化,另一個(gè)問題是數(shù)據(jù)都是 boxed 的,每次讀取都需要通過類似 mdata.num 和 mdata.dbl 的方法來間接獲取。
對(duì)于這樣的問題,就得靠 JIT 來優(yōu)化了
實(shí)現(xiàn) JIT 及優(yōu)化
首先值得一提的是 PHP 的 JIT 之前并非沒人嘗試過
- 2008 年就有人用 LLVM實(shí)驗(yàn)過,結(jié)果還比原來慢了 21 倍。。。
- 2010 年 IBM 日本研究院基于他們的 JVM 虛擬機(jī)代碼開發(fā)了 P9,性能是官方 PHP 的 2.5 到 9.5 倍,可以看他們的論文Evaluation of a just-in-time compiler retrofitted for PHP。
- 2011 年 Andrei Homescu 基于 RPython 開發(fā)過,還寫了篇論文 HappyJIT: a tracing JIT compiler for PHP,但測(cè)試結(jié)果有好有壞,并不理想。
那么究竟什么是 JIT?如何實(shí)現(xiàn)一個(gè) JIT?
在動(dòng)態(tài)語言中基本上都會(huì)有個(gè) eval 方法,可以傳給它一段字符串來執(zhí)行,JIT 做的就是類似的事情,只不過它要拼接不是字符串,而是不同平臺(tái)下的機(jī)器碼,然后進(jìn)行執(zhí)行,但如何用 C 來實(shí)現(xiàn)呢?可以參考 Eli 寫的這個(gè)入門例子,以下是文中的一段代碼:
<code>unsigned char code[] = { 0x48, 0x89, 0xf8, // mov %rdi, %rax 0x48, 0x83, 0xc0, 0x04, // add $4, %rax 0xc3 // ret }; memcpy(m, code, sizeof(code)); </code>
然而手工編寫機(jī)器碼很容易出錯(cuò),所以最好的有一個(gè)輔助的庫,比如的 Mozilla 的 Nanojit 以及 LuaJIT 的 DynASM,但 HHVM 并沒有使用這些,而是自己實(shí)現(xiàn)了一個(gè)只支持 x64 的(另外還在嘗試用 VIXL 來生成 ARM 64 位的),通過 mprotect 的方式來讓代碼可執(zhí)行。
但為什么 JIT 代碼會(huì)更快?你可以想想其實(shí)用 C++ 編寫的代碼最終編譯出來也是機(jī)器碼,如果只是將同樣的代碼手動(dòng)轉(zhuǎn)成了機(jī)器碼,那和 GCC 生成出來的有什么區(qū)別呢?雖然前面我們提到了一些針對(duì) CPU 實(shí)現(xiàn)原理來優(yōu)化的技巧,但在 JIT 中更重要的優(yōu)化是根據(jù)類型來生成特定的指令,從而大幅減少指令數(shù)和條件判斷,下面這張來自 TraceMonkey 的圖對(duì)此進(jìn)行了很直觀的對(duì)比,后面我們將看到 HHVM 中的具體例子:
HHVM 首先通過 interpeter 來執(zhí)行,那它會(huì)在時(shí)候使用 JIT 呢?常見的 JIT 觸發(fā)條件有 2 種:
- trace:記錄循環(huán)執(zhí)行次數(shù),如果超過一定數(shù)量就對(duì)這段代碼進(jìn)行 JIT
- method:記錄函數(shù)執(zhí)行次數(shù),如果超過一定數(shù)量就對(duì)整個(gè)函數(shù)進(jìn)行 JIT,甚至直接 inline
關(guān)于這兩種方法哪種更好在 Lambada 上有個(gè)帖子引來了各路大神的討論,尤其是 Mike Pall(LuaJIT 作者) 、Andreas Gal(Mozilla VP) 和 Brendan Eich(Mozilla CTO)都發(fā)表了很多自己的觀點(diǎn),推薦大家圍觀,我這里就不獻(xiàn)丑了。
它們之間的區(qū)別不僅僅是編譯范圍,還有很多細(xì)節(jié)問題,比如對(duì)局部變量的處理,在這里就不展開了
但 HHVM 并沒有采用這兩種方式,而是自創(chuàng)了一個(gè)叫 tracelet 的做法,它是根據(jù)類型來劃分的,看下面這張圖
可以看到它將一個(gè)函數(shù)劃分為了 3 部分,上面 2 部分是用于處理 $k
為整數(shù)或字符串兩種不同情況的,下面的部分是返回值,所以看起來它主要是根據(jù)類型的變化情況來劃分 JIT 區(qū)域的,具體是如何分析和拆解 Tracelet 的細(xì)節(jié)可以查看 Translator.cpp 中的 Translator::analyze
方法,我還沒空看,這里就不討論了。
當(dāng)然,要實(shí)現(xiàn)高性能的 JIT 還需進(jìn)行各種嘗試和優(yōu)化,比如最初 HHVM 新增的 tracelet 會(huì)放到前面,也就是將上圖的 A 和 C 調(diào)換位置,后來嘗試了一下放到后面,結(jié)果性能提示了 14%,因?yàn)闇y(cè)試發(fā)現(xiàn)這樣更容易提前命中響應(yīng)的類型
JIT 的執(zhí)行過程是首先將 HHBC 轉(zhuǎn)成 SSA (hhbc-translator.cpp),然后對(duì) SSA 上做優(yōu)化(比如 Copy propagation),再生成本地機(jī)器碼,比如在 X64 下是由 translator-x64.cpp 實(shí)現(xiàn)的。
我們用一個(gè)簡(jiǎn)單的例子來看看 HHVM 最終生成的機(jī)器碼是怎樣的,比如下面這個(gè) PHP 函數(shù):
<code><?php function a($b){ echo $b + 2; } </code></code>
編譯后是這個(gè)樣子:
<code>mov rcx,0x7200000 mov rdi,rbp mov rsi,rbx mov rdx,0x20 call 0x2651dfb <:transl::tracecallback hphp::typedvalue long void> cmp BYTE PTR [rbp-0x8],0xa jne 0xae00306 ; 前面是檢查參數(shù)是否有效 mov rcx,QWORD PTR [rbp-0x10] ; 這里將 %rcx 被賦值為1了 mov edi,0x2 ; 將 %edi(也就是 %rdi 的低32位)賦值為2 add rdi,rcx ; 加上 %rcx call 0x2131f1b <:print_int> ; 調(diào)用 print_int 函數(shù),這時(shí)第一個(gè)參數(shù) %rdi 的值已經(jīng)是3了 ; 后面暫不討論 mov BYTE PTR [rbp+0x28],0x8 lea rbx,[rbp+0x20] test BYTE PTR [r12],0xff jne 0xae0032a push QWORD PTR [rbp+0x8] mov rbp,QWORD PTR [rbp+0x0] mov rdi,rbp mov rsi,rbx mov rdx,QWORD PTR [rsp] call 0x236b70e <:jit::traceret hphp::typedvalue void> ret </:jit::traceret></:print_int></:transl::tracecallback></code>
而 HPHP::print_int 函數(shù)的實(shí)現(xiàn)是這樣的:
<code>void print_int(int64_t i) { char buf[256]; snprintf(buf, 256, "%" PRId64, i); echo(buf); TRACE(1, "t-x64 output(int): %" PRId64 "\n", i); } </code>
可以看到 HHVM 編譯出來的代碼直接使用了 int64_t,避免了 interpreter 中需要判斷參數(shù)和間接取數(shù)據(jù)的問題,從而明顯提升了性能,最終甚至做到了和 C 編譯出來的代碼區(qū)別不大。
需要注意:HHVM 在 server mode 下,只有超過12個(gè)請(qǐng)求就才會(huì)觸發(fā) JIT,啟動(dòng)過 HHVM 時(shí)可以通過加上如下參數(shù)來讓它首次請(qǐng)求就使用 JIT:
<code>-v Eval.JitWarmupRequests=0 </code>
所以在測(cè)試性能時(shí)需要注意,運(yùn)行一兩次就拿來對(duì)比是看不出效果的。
類型推導(dǎo)很麻煩,還是逼迫程序員寫清楚吧
JIT 的關(guān)鍵是猜測(cè)類型,因此某個(gè)變量的類型要是老變就很難優(yōu)化,于是 HHVM 的工程師開始考慮在 PHP 語法上做手腳,加上類型的支持,推出了一個(gè)新語言 - Hack(吐槽一下這名字真不利于 SEO),它的樣子如下:
<code><?php class Point2 { public float $x, $y; function __construct(float $x, float $y) { $this->x = $x; $this->y = $y; } } //來自:https://raw.github.com/strangeloop/StrangeLoop2013/master/slides/sessions/Adams-TakingPHPSeriously.pdf </code>
注意到 float
關(guān)鍵字了么?有了靜態(tài)類型可以讓 HHVM 更好地優(yōu)化性能,但這也意味著和 PHP 語法不兼容,只能使用 HHVM。
其實(shí)我個(gè)人認(rèn)為這樣做最大的優(yōu)點(diǎn)是讓代碼更加易懂,減少無意的犯錯(cuò),就像 Dart 中的可選類型也是這個(gè)初衷,同時(shí)還方便了 IDE 識(shí)別,據(jù)說 Facebook 還在開發(fā)一個(gè)基于Web的IDE,能協(xié)同編輯代碼,可以期待一下
你會(huì)使用 HHVM 么?
總的來說,比起之前的 HPHPc,我認(rèn)為 HHVM 是值得一試的,它是真正的虛擬機(jī),能夠更好地支持各種 PHP 的語法,所以改動(dòng)成本不會(huì)更高,而且因?yàn)槟軣o縫切換到官方 PHP 版本,所以可以同時(shí)啟動(dòng) FPM 來隨時(shí)待命,HHVM 還有 FastCGI 接口方便調(diào)用,只要做好應(yīng)急備案,風(fēng)險(xiǎn)是可控的,從長(zhǎng)遠(yuǎn)來看是很有希望的。
性能究竟能提升多少我無法確定,需要拿自己的業(yè)務(wù)代碼來進(jìn)行真實(shí)測(cè)試,這樣才能真正清楚 HHVM 能帶來多少收益,尤其是對(duì)整體性能提升到底有多少,只有拿到這個(gè)數(shù)據(jù)才能做決策。
最后整理一下可能會(huì)遇到的問題,有計(jì)劃使用的可以參考:
- 擴(kuò)展問題:如果用到了 PHP 擴(kuò)展,肯定是要重寫的,不過 HHVM 擴(kuò)展寫起來比 Zend 要簡(jiǎn)單的多,具體細(xì)節(jié)可以看 wiki 上的例子。
- HHVM Server 的穩(wěn)定性問題:這種多線程的架構(gòu)運(yùn)行一段時(shí)間可能會(huì)出現(xiàn)內(nèi)存泄露問題,或者某個(gè)沒寫好的 PHP 直接導(dǎo)致整個(gè)進(jìn)程掛掉,所以需要注意這方面的測(cè)試和容災(zāi)措施。
- 問題修復(fù)困難:HHVM 在出現(xiàn)問題時(shí)將比 Zend 難修復(fù),尤其是 JIT 的代碼,只能期望它比較穩(wěn)定了。
P.S. 其實(shí)我只了解基本的虛擬機(jī)知識(shí),也沒寫過幾行 PHP 代碼,很多東西都是寫這篇文章時(shí)臨時(shí)去找資料的,由于時(shí)間倉促水平有限,必然會(huì)有不正確的地方,歡迎大家評(píng)論賜教 :)
2014年1月補(bǔ)充:目前 HHVM 在鄙廠的推廣勢(shì)頭很不錯(cuò),推薦大家在2014年嘗試一下,尤其是現(xiàn)在兼容性測(cè)試已經(jīng)達(dá)到98.58%了,修改成本進(jìn)一步減小。
引用
Andrei Alexandrescu on AMA
Keith Adams 在 HN 上的蛛絲馬跡
How Three Guys Rebuilt the Foundation of Facebook
PHP on the Metal with HHVM
Making HPHPi Faster
HHVM Optimization Tips
The HipHop Virtual Machine (hhvm) PHP Execution at the Speed of JIT
Julien Verlaguet, Facebook: Analyzing PHP statically
Speeding up PHP-based development with HHVM
Adding an opcode to HHBC
原文: http://wuduoyi.com/note/hhvm/
原文地址:HHVM 是如何提升 PHP 性能的[轉(zhuǎn)], 感謝原作者分享。

Hot AI Tools

Undress AI Tool
Undress images for free

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Clothoff.io
AI clothes remover

Video Face Swap
Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Article

Hot Tools

Notepad++7.3.1
Easy-to-use and free code editor

SublimeText3 Chinese version
Chinese version, very easy to use

Zend Studio 13.0.1
Powerful PHP integrated development environment

Dreamweaver CS6
Visual web development tools

SublimeText3 Mac version
God-level code editing software (SublimeText3)

Hot Topics

The method to get the current session ID in PHP is to use the session_id() function, but you must call session_start() to successfully obtain it. 1. Call session_start() to start the session; 2. Use session_id() to read the session ID and output a string similar to abc123def456ghi789; 3. If the return is empty, check whether session_start() is missing, whether the user accesses for the first time, or whether the session is destroyed; 4. The session ID can be used for logging, security verification and cross-request communication, but security needs to be paid attention to. Make sure that the session is correctly enabled and the ID can be obtained successfully.

To extract substrings from PHP strings, you can use the substr() function, which is syntax substr(string$string,int$start,?int$length=null), and if the length is not specified, it will be intercepted to the end; when processing multi-byte characters such as Chinese, you should use the mb_substr() function to avoid garbled code; if you need to intercept the string according to a specific separator, you can use exploit() or combine strpos() and substr() to implement it, such as extracting file name extensions or domain names.

UnittestinginPHPinvolvesverifyingindividualcodeunitslikefunctionsormethodstocatchbugsearlyandensurereliablerefactoring.1)SetupPHPUnitviaComposer,createatestdirectory,andconfigureautoloadandphpunit.xml.2)Writetestcasesfollowingthearrange-act-assertpat

In PHP, the most common method is to split the string into an array using the exploit() function. This function divides the string into multiple parts through the specified delimiter and returns an array. The syntax is exploit(separator, string, limit), where separator is the separator, string is the original string, and limit is an optional parameter to control the maximum number of segments. For example $str="apple,banana,orange";$arr=explode(",",$str); The result is ["apple","bana

JavaScript data types are divided into primitive types and reference types. Primitive types include string, number, boolean, null, undefined, and symbol. The values are immutable and copies are copied when assigning values, so they do not affect each other; reference types such as objects, arrays and functions store memory addresses, and variables pointing to the same object will affect each other. Typeof and instanceof can be used to determine types, but pay attention to the historical issues of typeofnull. Understanding these two types of differences can help write more stable and reliable code.

std::chrono is used in C to process time, including obtaining the current time, measuring execution time, operation time point and duration, and formatting analysis time. 1. Use std::chrono::system_clock::now() to obtain the current time, which can be converted into a readable string, but the system clock may not be monotonous; 2. Use std::chrono::steady_clock to measure the execution time to ensure monotony, and convert it into milliseconds, seconds and other units through duration_cast; 3. Time point (time_point) and duration (duration) can be interoperable, but attention should be paid to unit compatibility and clock epoch (epoch)

In PHP, to pass a session variable to another page, the key is to start the session correctly and use the same $_SESSION key name. 1. Before using session variables for each page, it must be called session_start() and placed in the front of the script; 2. Set session variables such as $_SESSION['username']='JohnDoe' on the first page; 3. After calling session_start() on another page, access the variables through the same key name; 4. Make sure that session_start() is called on each page, avoid outputting content in advance, and check that the session storage path on the server is writable; 5. Use ses

ToaccessenvironmentvariablesinPHP,usegetenv()orthe$_ENVsuperglobal.1.getenv('VAR_NAME')retrievesaspecificvariable.2.$_ENV['VAR_NAME']accessesvariablesifvariables_orderinphp.iniincludes"E".SetvariablesviaCLIwithVAR=valuephpscript.php,inApach
