天天看點

Hack on HHVM —— Facebook是如何優化PHP的

Facebook周四正式釋出了 Hack 程式設計語言,将靜态類型以及一些現代的語言特性引入了PHP。這是Facebook對PHP優化之路上的新裡程碑。

Hack on HHVM —— Facebook是如何優化PHP的

Facebook為何要優化PHP

這個問題可以從不同角度來回答。簡單直接的回答是,Facebook的規模太大了。PHP的性能問題限制了Facebook的發展。從另一個角度來回答,則是要回答既然PHP不夠用,為什麼不幹脆換掉?

把PHP換掉也有“整體換”和“局部換”的差別。最徹底的方案就是完全離開PHP,用别的語言重寫一套。但是對于Facebook而言這個代價太高了。如果切換的話,多年來在PHP的積累就完全廢棄了。而且Facebook的業務邏輯非常複雜,據說PHP代碼有2千萬行…… 而且,Facebook員工衆多,切換到一種新的語言,學習成本也不低。

既然整體換不可行,那就局部換吧。例如給PHP寫C/C++擴充,可以提升性能。但是PHP擴充開發起來成本高,一般隻适用于比較穩定的庫,适用範圍很有限。另一個方案将性能瓶頸的地方用其他語言實作,然後通過RPC(Remote Procedure Call,遠端過程調用)在PHP和其他語言之間通訊。Twitter就用了這條路線,大量元件使用Scala和Java編寫,通過RPC與展現層的Rails通訊。事實上,Facebook在這方面已經做了不少工作,為了減少RPC調用的開銷,Facebook還專門開發了

Thrift

。然而,C++開發成本比PHP高很多,不适合用在需要快速修改的地方,而且大量RPC調用終究會影響性能。

整體換不現實,Thrift不夠用,那麼Facebook優化PHP就勢在必行了。

Facebook要如何優化PHP

優化PHP,最先想到的是作性能分析,找出瓶頸,然後進行對應的優化。Facebook為此開發了

XHProf

工具。XHProf精确到函數層面,資料收集元件使用C開發(PHP擴充),報告元件用了PHP。支援PHP 5.2以上版本,對于定位性能瓶頸很有幫助。

但是PHP語言層面的優化限制太多,對Facebook而言還是不夠用。是以Facebook需要對PHP語言的實作本身進行優化。

首先可以考慮的方案是改善PHP的官方實作。PHP的官方解釋器運作PHP代碼的過程可以分為兩步:第一步,将PHP編譯為bytecode;第二步,運作bytecode。那麼改善PHP的官方實作就可以從這兩個方面着手。

首先是優化編譯PHP的步驟,這方面的工作已經有

ZendOptimizerPlus

做了。它會在記憶體中緩存編譯好的bytecode,這樣以後通路代碼的時候就可以直接通路緩存好了的bytecode,省去了從磁盤讀取再重新編譯的開銷。但是由于PHP語言的動态性,這個方法的效果一般,最好的情況下也隻能提升20%的性能。

其次是優化運作bytecode的步驟。上面提到的ZendOptimizerPlus主要是優化編譯PHP,但是也附帶做了一些bytecode運作的優化。PHP有三種方式來運作bytecode:CALL、SWITCH和GOTO,預設使用CALL,也就是函數調用。優化函數調用,常用的方法就是内聯(Inline function),也就是将函數展開,将函數體插入替換調用該函數的地方,這樣可以節省每次調用函數帶來的額外時間開支。但是這種做法其實是用“空間換時間”,如果内聯過頭了,空間開銷會很大,得不償失。在這方面進行調整,可以提升運作bytecode的性能。

此外還可以将整個PHP解釋器用彙編重寫,以快聞名的

LuaJIT

就是這麼幹的。

然而,無論是内聯優化還是用彙編重寫,代價都很大,而且如果優化官方實作的話,還要考慮PHP的向下相容……

既然這個方案不太現實,那麼不如把PHP搬到JVM上吧?JVM性能相當不錯。

把PHP搬到JVM的工作,有人已經做過了。例如,IBM的

P8(已死)

Quercus

(半死不活)。Facebook也研究過這個方案,2012年的時候,還有Facebook遷移到JVM的傳聞。其實Facebook早已放棄這條路線。根據Facebook的研究,Quercus的性能和Zend+APC相比,差不了太多。這一方案效果不理想的原因可能是,JVM本身性能的優化是針對Java做的,其他語言在JVM上實作,不一定能用到這些優化。動态語言尤為如此。因為Java本身是靜态類型的,是以很多優化JVM就沒必要做,而在JVM上跑的動态語言需要這些優化。

既然JVM是為Java優化的,搬上去不合适,那不如針對PHP開發一個VM?這樣就可以作大量針對性地優化了。然而開發VM可沒有那麼容易,成本不小,是以Facebook最初的選擇是将PHP編譯成C/C++之類性能優異的語言。也就是HHVM的前身——HPHPc。具體的做法是将PHP翻譯為C++,然後再編譯。

相比VM,這樣的實作比較簡單,而且能放手做優化(因為是離線編譯,是以可以用時間換性能)。但是PHP的很多動态内容編譯成C++比較麻煩,是以HPHPc禁掉了

eval()

之類的特性,即使這樣,還是帶來了一些問題,特别是由于需要将動态include的檔案都編譯在一起,最終的部署檔案體積太龐大了,都過G了。

和HPHPc類似的項目有

Roadsend phc

,前者已經不維護了,後者也是命運坎坷。

編譯到C++的效果不好,是以Facebook最終決定,還是寫一個VM吧。

HHVM

FaceBook開發HHVM的陣容相當豪華,其中包括

  • Andrei Alexandrescu, 《C++ Coding Standards》的作者。
  • Drew Paroski,改進了.NET虛拟機的JIT。
  • Jason Evans,jemalloc的開發者(jemalloc将Firefox的記憶體消耗降低了一半)。
  • Keith Adams,VMware核心架構。
  • Sara Golemon,《Extending and Embedding PHP》作者,PHP核心領域的專家。

值得注意的是,Keith Adams給HHVM的影響很大。HHVM使用了JIT技術,一般的代碼通過解釋器執行(因為JIT也是有開銷的),而常用的代碼則使用JIT優化。通常而言,VM判斷是否需要進行JIT優化是通過以下兩種政策的一種:method-at-a-time(如果函數的執行超過了門檻值,就進行JIT優化)和tracing (如果循環的執行超過了門檻值,就進行JIT優化)。但是HHVM使用的是一種獨特的政策,basic-block-at-a-time,這個政策和VMware的x86 hypervisor相似。使用這個政策與Facebook希望支援類型推導的閉包有關。

上面提到了類型推導。事實上,Facebook推出了一個運作在HHVM上的PHP改良語言——Hack。Hack裡加入了類型的支援:

<?hh

class MyClass {

  const int MyConst = 0;

  private string $x = '';

  public function increment(int $x): int {

    $y = $x + 1;

    return $y;

  }

}

加了類型之後,除了友善大型團隊協作,避免程式設計中出現的錯誤之外,還有一個重要的原因就是能夠讓HHVM更好地優化性能。JIT優化最主要的方面就是根據類型來生成特定的指令,這樣可以減少大量的指令和條件判斷。而對于PHP這樣的動态語言,要推斷清楚類型是非常困難的,是以Hack就直接讓程式員寫上了。

相容性

HHVM除了作為Hack的VM之外,還可以運作原生的PHP。相容性測試表明,HHVM對PHP的相容度已經達到98.58%了。由于HHVM使用了獨特的JIT優化政策,是以Facebook自行開發了

tracelet

輔助庫,這個庫隻支援x86 64bit系統,是以HHVM也隻能在64位系統上使用——不過這個問題不大,現在的伺服器硬體基本都支援64位了。需要考慮的是PHP擴充的問題。由于PHP語言包含非常之多的擴充,而Facebook的HHVM隻實作了自家用到的擴充,是以可能有為HHVM重寫PHP擴充的需要。好在相比為官方PHP實作寫擴充,為HHVM寫擴充比較容易,對性能要求不高的擴充可以使用純PHP編寫,然後編譯到HHVM二進制檔案中即可,詳見

HHVM wiki

。還有一個要小心的問題就是HHVM是常駐記憶體的,是以如果某處PHP代碼有記憶體洩露問題的話,可能拖慢整個HHVM服務的速度,甚至導緻HHVM挂掉。