PHP已經(jīng)走過了20年的歷史,直到今天,PHP7都發(fā)布了RC版,據(jù)說,PHP7正式版應(yīng)該會在2015年11月份左右發(fā)布。PHP7對于上一個系列的PHP5.*,可以說是一個大規(guī)模的革新,尤其是在性能方面實現(xiàn)跨越式的大幅提升。

PHP是一種在全球范圍內(nèi)被廣泛使用的Web開發(fā)語言,PHP7的革新也當(dāng)然會給這些Web服務(wù)帶來更深刻的變化。這里引用鳥哥PPT中的一個圖表(82%的Web站點有使用PHP作為開發(fā)語言):

php7-features-1

(注:一個web站點可以會使用多種語言作為它的開發(fā)語言)

(注:本文含有不少從鳥哥PPT里的截圖,圖片版權(quán)歸鳥哥所有)

我們先看看兩張激動人心的性能測試結(jié)果圖:

Benchmark對比(圖片來自于PPT):

php7-features-2

PHP7的性能測試結(jié)果,性能壓測結(jié)果,耗時從2.991下降到1.186,大幅度下降60%。

WordPress的QPS壓測(圖片來自于PPT):

php7-features-3

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

看完令人激動的性能測試結(jié)果對比,我們就進入正題哈。PHP7的新增特性很多,不過,我們會更聚焦于那些主要的變化。

一、新增特性和改變

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

PHP語言一個非常重要的特點就是“弱類型”,它讓PHP的程序變得非常容易編寫,新手接觸PHP能夠快速上手,不過,它也伴隨著一些爭議。支持變 量類型的定義,可以說是革新性質(zhì)的變化,PHP開始以可選的方式支持類型定義。除此之外,還引入了一個開關(guān)指令 declare(strict_type=1);,當(dāng)這個指令一旦開啟,將會強制當(dāng)前文件下的程序遵循嚴(yán)格的函數(shù)傳參類型和返回類型。

例如一個add函數(shù)加上類型定義,可以寫成這樣:

php7-features-4

如果配合強制類型開關(guān)指令,則可以變?yōu)檫@樣:

php7-features-5

如果不開啟strict_type,PHP將會嘗試幫你轉(zhuǎn)換成要求的類型,而開啟之后,會改變PHP就不再做類型轉(zhuǎn)換,類型不匹配就會拋出錯誤。對于喜歡“強類型”語言的同學(xué)來說,這是一大福音。

更為詳細的介紹:PHP7標(biāo)量類型聲明RFC[翻譯]  

2. 更多的Error變?yōu)榭刹东@的Exception

PHP7實現(xiàn)了一個全局的throwable接口,原來的Exception和部分Error都實現(xiàn)了這個接口(interface), 以接口的 方式定義了異常的繼承結(jié)構(gòu)。于是,PHP7中更多的Error變?yōu)榭刹东@的Exception返回給開發(fā)者,如果不進行捕獲則為Error,如果捕獲就變 為一個可在程序內(nèi)處理的Exception。這些可被捕獲的Error通常都是不會對程序造成致命傷害的Error,例如函數(shù)不存。PHP7進一步方便開 發(fā)者處理,讓開發(fā)者對程序的掌控能力更強。因為在默認(rèn)情況下,Error會直接導(dǎo)致程序中斷,而PHP7則提供捕獲并且處理的能力,讓程序繼續(xù)執(zhí)行下去, 為程序員提供更靈活的選擇。

例如,執(zhí)行一個我們不確定是否存在的函數(shù),PHP5兼容的做法是在函數(shù)被調(diào)用之前追加的判斷function_exist,而PHP7則支持捕獲Exception的處理方式。

如下圖中的例子(截圖來源于PPT內(nèi)):

php7-features-6

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

AST在PHP編譯過程作為一個中間件的角色,替換原來直接從解釋器吐出opcode的方式,讓解釋器(parser)和編譯器(compliler)解耦,可以減少一些Hack代碼,同時,讓實現(xiàn)更容易理解和可維護。

PHP5:

php7-features-7

PHP7:

php7-features-8

更多AST信息:https://wiki.php.net/rfc/abstract_syntax_tree

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

PHP在多線程模式下(例如,Web服務(wù)器Apache的woker和event模式,就是多線程),需要解決“線程安全” (TS,Thread Safe)的問題,因為線程是共享進程的內(nèi)存空間的,所以每個線程本身需要通過某種方式,構(gòu)建私有的空間來保存自己的私有數(shù)據(jù),避 免和其他線程相互污染。而PHP5采用的方式,就是維護一個全局大數(shù)組,為每一個線程分配一份獨立的存儲空間,線程通過各自擁有的key值來訪問這個全局 數(shù)據(jù)組。

而這個獨有的key值在PHP5中需要傳遞給每一個需要用到全局變量的函數(shù),PHP7認(rèn)為這種傳遞的方式并不友好,并且存在一些問題。因而,嘗試采用一個全局的線程特定變量來保存這個key值。

相關(guān)的Native TLS問題:https://wiki.php.net/rfc/native-tls

5. 其他新特性

PHP7新特性和變化不少,我們這里并不全部展開來細說哈。

  1. Int64支持,統(tǒng)一不同平臺下的整型長度,字符串和文件上傳都支持大于2GB。

  2. 統(tǒng)一變量語法(Uniform variable syntax)。

  3. foreach表現(xiàn)行為一致(Consistently foreach behaviors)

  4. 新的操作符 <=>, ??

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

  6. 匿名類支持(Anonymous Class)

… …

#p#

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

1. JIT與性能

Just In Time(即時編譯)是一種軟件優(yōu)化技術(shù),指在運行時才會去編譯字節(jié)碼為機器碼。從直覺出發(fā),我們都很容易認(rèn)為,機器碼是計算機能 夠直接識別和執(zhí)行的,比起Zend讀取opcode逐條執(zhí)行效率會更高。其中,HHVM(HipHop Virtual Machine,HHVM是一個 Facebook開源的PHP虛擬機)就采用JIT,讓他們的PHP性能測試提升了一個數(shù)量級,放出一個令人震驚的測試結(jié)果,也讓我們直觀地認(rèn)為JIT是 一項點石成金的強大技術(shù)。

而實際上,在2013年的時候,鳥哥和Dmitry(PHP語言內(nèi)核開發(fā)者之一)就曾經(jīng)在PHP5.5的版本上做過一個JIT的嘗試(并沒有發(fā) 布)。PHP5.5的原來的執(zhí)行流程,是將PHP代碼通過詞法和語法分析,編譯成opcode字節(jié)碼(格式和匯編有點像),然后,Zend引擎讀取這些 opcode指令,逐條解析執(zhí)行。

php7-features-9

而他們在opcode環(huán)節(jié)后引入了類型推斷(TypeInf),然后通過JIT生成ByteCodes,然后再執(zhí)行。

php7-features-10

于是,在benchmark(測試程序)中得到令人興奮的結(jié)果,實現(xiàn)JIT后性能比PHP5.5提升了8倍。然而,當(dāng)他們把這個優(yōu)化放入到實際的項目WordPress(一個開源博客項目)中,卻幾乎看不見性能的提升,得到了一個令人費解的測試結(jié)果。

于是,他們使用Linux下的profile類型工具,對程序執(zhí)行進行CPU耗時占用分析。

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

php7-features-11

注解:

  • 21%CPU時間花費在內(nèi)存管理。

  • 12%CPU時間花費在hash table操作,主要是PHP數(shù)組的增刪改查。

  • 30%CPU時間花費在內(nèi)置函數(shù),例如strlen。

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

經(jīng)過分析之后,得到了兩個結(jié)論:

(1)JIT生成的ByteCodes如果太大,會引起CPU緩存***率下降(CPU Cache Miss)

在PHP5.5的代碼里,因為并沒有明顯類型定義,只能靠類型推斷。盡可能將可以推斷出來的變量類型,定義出來,然后,結(jié)合類型推斷,將非該類型的 分支代碼去掉,生成直接可執(zhí)行的機器碼。然而,類型推斷不能推斷出全部類型,在WordPress中,能夠推斷出來的類型信息只有不到30%,能夠減少的 分支代碼有限。導(dǎo)致JIT以后,直接生成機器碼,生成的ByteCodes太大,最終引起CPU緩存***大幅度下降(CPU Cache Miss)。

CPU緩存***是指,CPU在讀取并執(zhí)行指令的過程中,如果需要的數(shù)據(jù)在CPU一級緩存(L1)中讀取不到,就不得不往下繼續(xù)尋找,一直到二級緩存 (L2)和三級緩存(L3),最終會嘗試到內(nèi)存區(qū)域里尋找所需要的指令數(shù)據(jù),而內(nèi)存和CPU緩存之間的讀取耗時差距可以達到100倍級別。所 以,ByteCodes如果過大,執(zhí)行指令數(shù)量過多,導(dǎo)致多級緩存無法容納如此之多的數(shù)據(jù),部分指令將不得不被存放到內(nèi)存區(qū)域。

php7-features-12

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

php7-features-13

因此,CPU緩存***率下降會帶來嚴(yán)重的耗時增加,另一方面,JIT帶來的性能提升,也被它所抵消掉了。

通過JIT,可以降低VM的開銷,同時,通過指令優(yōu)化,可以間接降低內(nèi)存管理的開發(fā),因為可以減少內(nèi)存分配的次數(shù)。然而,對于真實的 WordPress項目來說,CPU耗時只有25%在VM上,主要的問題和瓶頸實際上并不在VM上。因此,JIT的優(yōu)化計劃,***沒有被列入該版本的 PHP7特性中。不過,它很可能會在更后面的版本中實現(xiàn),這點也非常值得我們期待哈。

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

JIT在benchmark中有大幅度的提升,是因為代碼量比較少,最終生成的ByteCodes也比較小,同時主要的開銷是在VM中。而應(yīng)用在 WordPress實際項目中并沒有明顯的性能提升,原因WordPress的代碼量要比benchmark大得多,雖然JIT降低了VM的開銷,但是因 為ByteCodes太大而又引起CPU緩存***下降和額外的內(nèi)存開銷,最終變成沒有提升。

不同類型的項目會有不同的CPU開銷比例,也會得到不同的結(jié)果,脫離實際項目的性能測試,并不具有很好的代表性。

2. Zval的改變

PHP的各種類型的變量,其實,真正存儲的載體就是Zval,它特點是海納百川,有容乃大。從本質(zhì)上看,它是C語言實現(xiàn)的一個結(jié)構(gòu)體(struct)。對于寫PHP的同學(xué),可以將它粗略理解為是一個類似array數(shù)組的東西。

PHP5的Zval,內(nèi)存占據(jù)24個字節(jié)(截圖來自PPT):

php7-features-14

PHP7的Zval,內(nèi)存占據(jù)16個字節(jié)(截圖來自PPT):

php7-features-15

Zval從24個字節(jié)下降到16個字節(jié),為什么會下降呢,這里需要補一點點的C語言基礎(chǔ),輔助不熟悉C的同學(xué)理解。struct和union(聯(lián)合 體)有點不同,Struct的每一個成員變量要各自占據(jù)一塊獨立的內(nèi)存空間,而union里的成員變量是共用一塊內(nèi)存空間(也就是說修改其中一個成員變 量,公有空間就被修改了,其他成員變量的記錄也就沒有了)。因此,雖然成員變量看起來多了不少,但是實際占據(jù)的內(nèi)存空間卻下降了。

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

Zval結(jié)構(gòu)圖(來源于PPT中):

php7-features-16

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

對于簡單的變量類型來說,Zval的存儲變得非常簡單和高效。

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

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

3. 內(nèi)部類型zend_string

Zend_string是實際存儲字符串的結(jié)構(gòu)體,實際的內(nèi)容會存儲在val(char,字符型)中,而val是一個char數(shù)組,長度為1(方便成員變量占位)。

php7-features-17

結(jié)構(gòu)體***一個成員變量采用char數(shù)組,而不是使用char*,這里有一個小優(yōu)化技巧,可以降低CPU的cache miss。

如果使用char數(shù)組,當(dāng)malloc申請上述結(jié)構(gòu)體內(nèi)存,是申請在同一片區(qū)域的,通常是長度是sizeof(_zend_string) + 實際char存儲空間。但是,如果使用char*,那個這個位置存儲的只是一個指針,真實的存儲又在另外一片獨立的內(nèi)存區(qū)域內(nèi)。

使用char[1]和char*的內(nèi)存分配對比:

php7-features-18

從邏輯實現(xiàn)的角度來看,兩者其實也沒有多大區(qū)別,效果很類似。而實際上,當(dāng)這些內(nèi)存塊被載入到CPU的中,就顯得非常不一樣。前者因為是連續(xù)分配在 一起的同一塊內(nèi)存,在CPU讀取時,通常都可以一同獲得(因為會在同一級緩存中)。而后者,因為是兩塊內(nèi)存的數(shù)據(jù),CPU讀取***塊內(nèi)存的時候,很可能第 二塊內(nèi)存數(shù)據(jù)不在同一級緩存中,使CPU不得不往L2(二級緩存)以下尋找,甚至到內(nèi)存區(qū)域查到想要的第二塊內(nèi)存數(shù)據(jù)。這里就會引起 CPU Cache Miss,而兩者的耗時***可以相差100倍。

另外,在字符串復(fù)制的時候,采用引用賦值,zend_string可以避免的內(nèi)存拷貝。

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

在編寫PHP程序過程中,使用最頻繁的類型莫過于數(shù)組,PHP5的數(shù)組采用HashTable實現(xiàn)。如果用比較粗略的概括方式來說,它算是一個支持 雙向鏈表的HashTable,不僅支持通過數(shù)組的key來做hash映射訪問元素,也能通過foreach以訪問雙向鏈表的方式遍歷數(shù)組元素。

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

php7-features-19

這個圖看起來很復(fù)雜,各種指針跳來跳去,當(dāng)我們通過key值訪問一個元素內(nèi)容的時候,有時需要3次的指針跳躍才能找對需要的內(nèi)容。而最重要的一點, 就在于這些數(shù)組元素存儲,都是分散在各個不同的內(nèi)存區(qū)域的。同理可得,在CPU讀取的時候,因為它們就很可能不在同一級緩存中,會導(dǎo)致CPU不得不到下級 緩存甚至內(nèi)存區(qū)域查找,也就是引起CPU緩存***下降,進而增加更多的耗時。

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

php7-features-20

新版本的數(shù)組結(jié)構(gòu),非常簡潔,讓人眼前一亮。***的特點是,整塊的數(shù)組元素和hash映射表全部連接在一起,被分配在同一塊內(nèi)存內(nèi)。如果是遍歷一個 整型的簡單類型數(shù)組,效率會非常快,因為,數(shù)組元素(Bucket)本身是連續(xù)分配在同一塊內(nèi)存里,并且,數(shù)組元素的zval會把整型元素存儲在內(nèi)部,也 不再有指針外鏈,全部數(shù)據(jù)都存儲在當(dāng)前內(nèi)存區(qū)域內(nèi)。當(dāng)然,最重要的是,它能夠避免CPU Cache Miss(CPU緩存***率下降)。

Zend Array的變化:

  1. 數(shù)組的value默認(rèn)為zval。

  2. HashTable的大小從72下降到56字節(jié),減少22%。

  3. Buckets的大小從72下降到32字節(jié),減少50%。

  4. 數(shù)組元素的Buckets的內(nèi)存空間是一同分配的。

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

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

  7. 降低CPU Cache Miss。

7. 函數(shù)調(diào)用機制(Function Calling Convention)

PHP7改進了函數(shù)的調(diào)用機制,通過優(yōu)化參數(shù)傳遞的環(huán)節(jié),減少了一些指令,提高執(zhí)行效率。

PHP5的函數(shù)調(diào)用機制(截圖來自于PPT):

php7-features-21

圖中,在vm棧中的指令send_val和recv參數(shù)的指令是相同,PHP7通過減少這兩條重復(fù),來達到對函數(shù)調(diào)用機制的底層優(yōu)化。

PHP7的函數(shù)調(diào)用機制(截圖來自于PPT):

php7-features-22

8. 通過宏定義和內(nèi)聯(lián)函數(shù)(inline),讓編譯器提前完成部分工作

C語言的宏定義會被在預(yù)處理階段(編譯階段)執(zhí)行,提前將部分工作完成,無需在程序運行時分配內(nèi)存,能夠?qū)崿F(xiàn)類似函數(shù)的功能,卻沒有函數(shù)調(diào)用的壓 棧、彈棧開銷,效率會比較高。內(nèi)聯(lián)函數(shù)也類似,在預(yù)處理階段,將程序中的函數(shù)替換為函數(shù)體,真實運行的程序執(zhí)行到這里,就不會產(chǎn)生函數(shù)調(diào)用的開銷。

PHP7在這方面做了不少的優(yōu)化,將不少需要在運行階段要執(zhí)行的工作,放到了編譯階段。例如參數(shù)類型的判斷(Parameters Parsing),因為這里涉及的都是固定的字符常量,因此,可以放到到編譯階段來完成,進而提升后續(xù)的執(zhí)行效率。

例如下圖中處理傳遞參數(shù)類型的方式,從左邊的寫法,優(yōu)化為右邊宏的寫法。

php7-features-23

三、小結(jié)

鳥哥的PPT里放出過一組對比數(shù)據(jù),就是WordPress在PHP5.6執(zhí)行100次會產(chǎn)生70億次的CPU指令執(zhí)行數(shù)目,而在PHP7中只需要25億次,減少64.2%,這是一個令人震撼的數(shù)據(jù)。

在鳥哥的整個分享中,給我最深刻的一個觀點是:要注意細節(jié),很多個細小的優(yōu)化,一點點持續(xù)地積累,積少成多,最終匯聚為驚艷的成果。為山九仞,豈一日之功,我想大概也是這個道理。

毫無疑問,PHP7在性能方面實現(xiàn)跨越式的提升,如果能夠?qū)⑦@些成果應(yīng)用在PHP的Web系統(tǒng)中,也許我們只需要更少的機器,就可以支撐起更高請求量的服務(wù)。PHP7正式版的發(fā)布,令人充滿***憧憬。

參考&引用資料:

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

PHP官方社區(qū),http://php.net/

致謝:

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

via:http://hansionxu.blog.163.com/