PHP7革新與性能優化

有幸參與2015年的PHP技術峯會(PHPCON),聽了鳥哥(惠新宸)的關於PHP7的新特性和性能優化的分享,一切都使人感到激動。鳥哥是國內最權威的PHP專家,他的分享有不少很是有價值的東西,我經過整理分享的PPT和收集相關資料,整理爲這篇解讀性質的技術文章,但願能給作PHP開發的同窗一些幫助。php

 

PHP已經走過了20年的歷史,直到今天,PHP7都發布了RC版,聽說,PHP7正式版應該會在2015年11月份左右發佈。PHP7對於上一個系列的PHP5.*,能夠說是一個大規模的革新,尤爲是在性能方面實現跨越式的大幅提高。程序員

PHP是一種在全球範圍內被普遍使用的Web開發語言,PHP7的革新也固然會給這些Web服務帶來更深入的變化。這裏引用鳥哥PPT中的一個圖表(82%的Web站點有使用PHP做爲開發語言):web

 PHP7革新與性能優化 - 徐漢彬Hansion - 技術行者

(注:一個web站點能夠會使用多種語言做爲它的開發語言)數組

(注:本文含有很多從鳥哥PPT裏的截圖,圖片版權歸鳥哥全部)緩存

 

咱們先看看兩張激動人心的性能測試結果圖:安全

Benchmark對比(圖片來自於PPT):性能優化

 PHP7革新與性能優化 - 徐漢彬Hansion - 技術行者

PHP7的性能測試結果,性能壓測結果,耗時從2.991降低到1.186,大幅度降低60%。服務器

WordPress的QPS壓測(圖片來自於PPT):多線程

 PHP7革新與性能優化 - 徐漢彬Hansion - 技術行者

而在WordPress項目中,PHP7對比PHP5.6,QPS提高2.77倍。函數

看完使人激動的性能測試結果對比,咱們就進入正題哈。PHP7的新增特性不少,不過,咱們會更聚焦於那些主要的變化。

 

1、新增特性和改變

1. 標量類型和返回類型聲明(Scalar Type Declarations & Scalar Type Declarations)

PHP語言一個很是重要的特色就是「弱類型」,它讓PHP的程序變得很是容易編寫,新手接觸PHP可以快速上手,不過,它也伴隨着一些爭議。支持變量類型的定義,能夠說是革新性質的變化,PHP開始以可選的方式支持類型定義。除此以外,還引入了一個開關指令declare(strict_type=1);,當這個指令一旦開啓,將會強制當前文件下的程序遵循嚴格的函數傳參類型和返回類型。

例如一個add函數加上類型定義,能夠寫成這樣:

 PHP7革新與性能優化 - 徐漢彬Hansion - 技術行者

若是配合強制類型開關指令,則能夠變爲這樣:

 PHP7革新與性能優化 - 徐漢彬Hansion - 技術行者

若是不開啓strict_type,PHP將會嘗試幫你轉換成要求的類型,而開啓以後,會改變PHP就再也不作類型轉換,類型不匹配就會拋出錯誤。對於喜歡「強類型」語言的同窗來講,這是一大福音。

更爲詳細的介紹:

PHP7標量類型聲明RFC[翻譯]  

 

2. 更多的Error變爲可捕獲的Exception

PHP7實現了一個全局的throwable接口,原來的Exception和部分Error都實現了這個接口(interface), 以接口的方式定義了異常的繼承結構。因而,PHP7中更多的Error變爲可捕獲的Exception返回給開發者,若是不進行捕獲則爲Error,若是捕獲就變爲一個可在程序內處理的Exception。這些可被捕獲的Error一般都是不會對程序形成致命傷害的Error,例如函數不存。PHP7進一步方便開發者處理,讓開發者對程序的掌控能力更強。由於在默認狀況下,Error會直接致使程序中斷,而PHP7則提供捕獲而且處理的能力,讓程序繼續執行下去,爲程序員提供更靈活的選擇。

例如,執行一個咱們不肯定是否存在的函數,PHP5兼容的作法是在函數被調用以前追加的判斷function_exist,而PHP7則支持捕獲Exception的處理方式。

以下圖中的例子(截圖來源於PPT內):

 PHP7革新與性能優化 - 徐漢彬Hansion - 技術行者

 

3. AST(Abstract Syntax Tree,抽象語法樹)

AST在PHP編譯過程做爲一箇中間件的角色,替換原來直接從解釋器吐出opcode的方式,讓解釋器(parser)和編譯器(compliler)解耦,能夠減小一些Hack代碼,同時,讓實現更容易理解和可維護。

PHP5:

 PHP7革新與性能優化 - 徐漢彬Hansion - 技術行者 

PHP7:

 PHP7革新與性能優化 - 徐漢彬Hansion - 技術行者

更多AST信息:

https://wiki.php.net/rfc/abstract_syntax_tree

 

4. Native TLS(Native Thread local storage,原生線程本地存儲)

PHP在多線程模式下(例如,Web服務器Apache的woker和event模式,就是多線程),須要解決「線程安全」(TS,Thread Safe)的問題,由於線程是共享進程的內存空間的,因此每一個線程自己須要經過某種方式,構建私有的空間來保存本身的私有數據,避免和其餘線程相互污染。而PHP5採用的方式,就是維護一個全局大數組,爲每個線程分配一份獨立的存儲空間,線程經過各自擁有的key值來訪問這個全局數據組。

而這個獨有的key值在PHP5中須要傳遞給每個須要用到全局變量的函數,PHP7認爲這種傳遞的方式並不友好,而且存在一些問題。於是,嘗試採用一個全局的線程特定變量來保存這個key值。

相關的Native TLS問題:

https://wiki.php.net/rfc/native-tls

 

5. 其餘新特性

PHP7新特性和變化很多,咱們這裏並不所有展開來細說哈。

(1) Int64支持,統一不一樣平臺下的整型長度,字符串和文件上傳都支持大於2GB。

(2) 統一變量語法(Uniform variable syntax)。

(3) foreach表現行爲一致(Consistently foreach behaviors)

(4) 新的操做符 <=>, ??

(5) Unicode字符格式支持(\u{xxxxx})

(6) 匿名類支持(Anonymous Class)

… …

 

2、跨越式的性能突破:全速前進

1. JIT與性能

Just In Time(即時編譯)是一種軟件優化技術,指在運行時纔會去編譯字節碼爲機器碼。從直覺出發,咱們都很容易認爲,機器碼是計算機可以直接識別和執行的,比起Zend讀取opcode逐條執行效率會更高。其中,HHVM(HipHop Virtual Machine,HHVM是一個Facebook開源的PHP虛擬機)就採用JIT,讓他們的PHP性能測試提高了一個數量級,放出一個使人震驚的測試結果,也讓咱們直觀地認爲JIT是一項點石成金的強大技術。

而實際上,在2013年的時候,鳥哥和Dmitry(PHP語言內核開發者之一)就曾經在PHP5.5的版本上作過一個JIT的嘗試(並無發佈)。PHP5.5的原來的執行流程,是將PHP代碼經過詞法和語法分析,編譯成opcode字節碼(格式和彙編有點像),而後,Zend引擎讀取這些opcode指令,逐條解析執行。

 

PHP7革新與性能優化 - 徐漢彬Hansion - 技術行者

  

而他們在opcode環節後引入了類型推斷(TypeInf),而後經過JIT生成ByteCodes,而後再執行。

 PHP7革新與性能優化 - 徐漢彬Hansion - 技術行者

因而,在benchmark(測試程序)中獲得使人興奮的結果,實現JIT後性能比PHP5.5提高了8倍。然而,當他們把這個優化放入到實際的項目WordPress(一個開源博客項目)中,卻幾乎看不見性能的提高,獲得了一個使人費解的測試結果。

因而,他們使用Linux下的profile類型工具,對程序執行進行CPU耗時佔用分析。

執行100次WordPress的CPU消耗的分佈(截圖來自PPT):

 PHP7革新與性能優化 - 徐漢彬Hansion - 技術行者

註解:

21%CPU時間花費在內存管理。

12%CPU時間花費在hash table操做,主要是PHP數組的增刪改查。

30%CPU時間花費在內置函數,例如strlen。

25%CPU時間花費在VM(Zend引擎)。

 

通過分析以後,獲得了兩個結論:

(1)JIT生成的ByteCodes若是太大,會引發CPU緩存命中率降低(CPU Cache Miss)

在PHP5.5的代碼裏,由於並無明顯類型定義,只能靠類型推斷。儘量將能夠推斷出來的變量類型,定義出來,而後,結合類型推斷,將非該類型的分支代碼去掉,生成直接可執行的機器碼。然而,類型推斷不能推斷出所有類型,在WordPress中,可以推斷出來的類型信息只有不到30%,可以減小的分支代碼有限。致使JIT之後,直接生成機器碼,生成的ByteCodes太大,最終引發CPU緩存命中大幅度降低(CPU Cache Miss)。

CPU緩存命中是指,CPU在讀取並執行指令的過程當中,若是須要的數據在CPU一級緩存(L1)中讀取不到,就不得不往下繼續尋找,一直到二級緩存(L2)和三級緩存(L3),最終會嘗試到內存區域裏尋找所須要的指令數據,而內存和CPU緩存之間的讀取耗時差距能夠達到100倍級別。因此,ByteCodes若是過大,執行指令數量過多,致使多級緩存沒法容納如此之多的數據,部分指令將不得不被存放到內存區域。

 PHP7革新與性能優化 - 徐漢彬Hansion - 技術行者 

CPU的各級緩存的大小也是有限的,下圖是Intel i7 920的配置信息:

 PHP7革新與性能優化 - 徐漢彬Hansion - 技術行者

所以,CPU緩存命中率降低會帶來嚴重的耗時增長,另外一方面,JIT帶來的性能提高,也被它所抵消掉了。

 

經過JIT,能夠下降VM的開銷,同時,經過指令優化,能夠間接下降內存管理的開發,由於能夠減小內存分配的次數。然而,對於真實的WordPress項目來講,CPU耗時只有25%在VM上,主要的問題和瓶頸實際上並不在VM上。所以,JIT的優化計劃,最後沒有被列入該版本的PHP7特性中。不過,它極可能會在更後面的版本中實現,這點也很是值得咱們期待哈。

 

(2)JIT性能的提高效果取決於項目的實際瓶頸

JIT在benchmark中有大幅度的提高,是由於代碼量比較少,最終生成的ByteCodes也比較小,同時主要的開銷是在VM中。而應用在WordPress實際項目中並無明顯的性能提高,緣由WordPress的代碼量要比benchmark大得多,雖然JIT下降了VM的開銷,可是由於ByteCodes太大而又引發CPU緩存命中降低和額外的內存開銷,最終變成沒有提高。

不一樣類型的項目會有不一樣的CPU開銷比例,也會獲得不一樣的結果,脫離實際項目的性能測試,並不具備很好的表明性。

 

2. Zval的改變

PHP的各類類型的變量,其實,真正存儲的載體就是Zval,它特色是海納百川,有容乃大。從本質上看,它是C語言實現的一個結構體(struct)。對於寫PHP的同窗,能夠將它粗略理解爲是一個相似array數組的東西。

PHP5的Zval,內存佔據24個字節(截圖來自PPT):

 PHP7革新與性能優化 - 徐漢彬Hansion - 技術行者

PHP7的Zval,內存佔據16個字節(截圖來自PPT):

 PHP7革新與性能優化 - 徐漢彬Hansion - 技術行者

Zval從24個字節降低到16個字節,爲何會降低呢,這裏須要補一點點的C語言基礎,輔助不熟悉C的同窗理解。struct和union(聯合體)有點不一樣,Struct的每個成員變量要各自佔據一塊獨立的內存空間,而union裏的成員變量是共用一塊內存空間(也就是說修改其中一個成員變量,公有空間就被修改了,其餘成員變量的記錄也就沒有了)。所以,雖然成員變量看起來多了很多,可是實際佔據的內存空間卻降低了。

 

除此以外,還有被明顯改變的特性,部分簡單類型再也不使用引用。

Zval結構圖(來源於PPT中):

 PHP7革新與性能優化 - 徐漢彬Hansion - 技術行者

圖中Zval的由2個64bits(1字節=8bit,bit是「位」)組成,若是變量類型是long、bealoon這些長度不超過64bit的,則直接存儲到value中,就沒有下面的引用了。當變量類型是array、objec、string等超過64bit的,value存儲的就是一個指針,指向真實的存儲結構地址。

對於簡單的變量類型來講,Zval的存儲變得很是簡單和高效。

不須要引用的類型:NULL、Boolean、Long、Double

須要引用的類型:String、Array、Object、Resource、Reference

 

3. 內部類型zend_string

Zend_string是實際存儲字符串的結構體,實際的內容會存儲在val(char,字符型)中,而val是一個char數組,長度爲1(方便成員變量佔位)。

 PHP7革新與性能優化 - 徐漢彬Hansion - 技術行者

結構體最後一個成員變量採用char數組,而不是使用char*,這裏有一個小優化技巧,能夠下降CPU的cache miss。

若是使用char數組,當malloc申請上述結構體內存,是申請在同一片區域的,一般是長度是sizeof(_zend_string) + 實際char存儲空間。可是,若是使用char*,那個這個位置存儲的只是一個指針,真實的存儲又在另一片獨立的內存區域內。

使用char[1]和char*的內存分配對比:

 PHP7革新與性能優化 - 徐漢彬Hansion - 技術行者

從邏輯實現的角度來看,二者其實也沒有多大區別,效果很相似。而實際上,當這些內存塊被載入到CPU的中,就顯得很是不同。前者由於是連續分配在一塊兒的同一塊內存,在CPU讀取時,一般均可以一同得到(由於會在同一級緩存中)。然後者,由於是兩塊內存的數據,CPU讀取第一塊內存的時候,極可能第二塊內存數據不在同一級緩存中,使CPU不得不往L2(二級緩存)如下尋找,甚至到內存區域查到想要的第二塊內存數據。這裏就會引發CPU Cache Miss,而二者的耗時最高能夠相差100倍。

另外,在字符串複製的時候,採用引用賦值,zend_string能夠避免的內存拷貝。

 

6. PHP數組的變化(HashTable和Zend Array)

在編寫PHP程序過程當中,使用最頻繁的類型莫過於數組,PHP5的數組採用HashTable實現。若是用比較粗略的歸納方式來講,它算是一個支持雙向鏈表的HashTable,不只支持經過數組的key來作hash映射訪問元素,也能經過foreach以訪問雙向鏈表的方式遍歷數組元素。

PHP5的HashTable(截圖來自於PPT):

 PHP7革新與性能優化 - 徐漢彬Hansion - 技術行者

這個圖看起來很複雜,各類指針跳來跳去,當咱們經過key值訪問一個元素內容的時候,有時須要3次的指針跳躍才能找對須要的內容。而最重要的一點,就在於這些數組元素存儲,都是分散在各個不一樣的內存區域的。同理可得,在CPU讀取的時候,由於它們就極可能不在同一級緩存中,會致使CPU不得不到下級緩存甚至內存區域查找,也就是引發CPU緩存命中降低,進而增長更多的耗時。

PHP7的Zend Array(截圖來源於PPT):

 PHP7革新與性能優化 - 徐漢彬Hansion - 技術行者

新版本的數組結構,很是簡潔,讓人眼前一亮。最大的特色是,整塊的數組元素和hash映射表所有鏈接在一塊兒,被分配在同一塊內存內。若是是遍歷一個整型的簡單類型數組,效率會很是快,由於,數組元素(Bucket)自己是連續分配在同一塊內存裏,而且,數組元素的zval會把整型元素存儲在內部,也再也不有指針外鏈,所有數據都存儲在當前內存區域內。固然,最重要的是,它可以避免CPU Cache Miss(CPU緩存命中率降低)。

Zend Array的變化:

(1) 數組的value默認爲zval。

(2) HashTable的大小從72降低到56字節,減小22%。

(3) Buckets的大小從72降低到32字節,減小50%。

(4) 數組元素的Buckets的內存空間是一同分配的。

(5) 數組元素的key(Bucket.key)指向zend_string。

(6) 數組元素的value被嵌入到Bucket中。

(7) 下降CPU Cache Miss。

 

7. 函數調用機制(Function Calling Convention)

PHP7改進了函數的調用機制,經過優化參數傳遞的環節,減小了一些指令,提升執行效率。

PHP5的函數調用機制(截圖來自於PPT):

PHP7革新與性能優化 - 徐漢彬Hansion - 技術行者

圖中,在vm棧中的指令send_val和recv參數的指令是相同,PHP7經過減小這兩條重複,來達到對函數調用機制的底層優化。

PHP7的函數調用機制(截圖來自於PPT):

 PHP7革新與性能優化 - 徐漢彬Hansion - 技術行者

 

8. 經過宏定義和內聯函數(inline),讓編譯器提早完成部分工做

C語言的宏定義會被在預處理階段(編譯階段)執行,提早將部分工做完成,無需在程序運行時分配內存,可以實現相似函數的功能,卻沒有函數調用的壓棧、彈棧開銷,效率會比較高。內聯函數也相似,在預處理階段,將程序中的函數替換爲函數體,真實運行的程序執行到這裏,就不會產生函數調用的開銷。

PHP7在這方面作了很多的優化,將很多須要在運行階段要執行的工做,放到了編譯階段。例如參數類型的判斷(Parameters Parsing),由於這裏涉及的都是固定的字符常量,所以,能夠放到到編譯階段來完成,進而提高後續的執行效率。

例以下圖中處理傳遞參數類型的方式,從左邊的寫法,優化爲右邊宏的寫法。

 PHP7革新與性能優化 - 徐漢彬Hansion - 技術行者

 

3、小結

鳥哥的PPT裏放出過一組對比數據,就是WordPress在PHP5.6執行100次會產生70億次的CPU指令執行數目,而在PHP7中只須要25億次,減小64.2%,這是一個使人震撼的數據。

在鳥哥的整個分享中,給我最深入的一個觀點是:要注意細節,不少個細小的優化,一點點持續地積累,聚沙成塔,最終匯聚爲驚豔的成果。爲山九仞,豈一日之功,我想大概也是這個道理。

毫無疑問,PHP7在性能方面實現跨越式的提高,若是可以將這些成果應用在PHP的Web系統中,也許咱們只須要更少的機器,就能夠支撐起更高請求量的服務。PHP7正式版的發佈,使人充滿無限憧憬。

 

參考&引用資料:

鳥哥(惠新宸)的分享PPT,http://www.laruence.com/

PHP官方社區,http://php.net/

 

致謝:

感謝鳥哥(惠新宸)提供的幫助與支持。

 

原文:http://hansionxu.blog.163.com/blog/static/24169810920158704014772

相關文章
相關標籤/搜索