PHP已經走過了20年的歷史,PHP7對於上一個系列的PHP5,可以說是一個大規模的革新,尤其是在性能方面實現跨越式的大幅提升。 PHP是一種在全球廣泛使用的Web開發語言,PHP7的革新也當然會為這些Web服務帶來更深刻的改變。
這裡引用鳥哥PPT中的一個圖表(82%的Web網站有使用PHP作為開發語言):
(註:一個web網站可以會使用多種語言作為它的開發語言)
(註:本文含有不少從鳥哥PPT裡的截圖,圖片版權歸鳥哥所有)
#我們先看看兩張令人興奮的效能測試結果圖
PHP7的效能測試結果,效能壓測結果,耗時從2.991下降到1.186,大幅下降60%。
WordPress的QPS壓測(圖片來自於PPT):
而在WordPress專案中,PHP7對比PHP5.6,QPS提升2.77倍。
看完令人興奮的效能測試結果對比,我們就進入正題哈。 PHP7的新增特性很多,不過,我們會更聚焦在那些主要的變化。
一、新增特性與改變
1. 標量型別與傳回型別宣告(Scalar Type Declarations & Scalar Type Declarations)
#PHP語言一個非常重要的特點是“弱類型”,它讓PHP的程式變得非常容易編寫,新手接觸PHP能夠快速上手,不過,它也伴隨著一些爭議。支持變數類型的定義,可以說是革新性質的變化,PHP開始以可選的方式支援類型定義。除此之外,還引入了一個開關指令declare(strict_type=1);,當這個指令一旦開啟,將會強制當前文件下的程序遵循嚴格的函數傳參類型和返回類型。
例如一個add函數加上類型定義,可以寫成這樣:
如果配合強制類型開關指令,則可以變成這樣:
如果不開啟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內):
3. AST(Abstract Syntax Tree,抽象語法樹)
AST在PHP編譯過程作為一個中間件的角色,替換原來直接從解釋器吐出opcode的方式,讓解釋器(parser)和編譯器(compliler)解耦,可以減少一些Hack程式碼,同時,讓實現更容易理解和可維護。
PHP5:
PHP7:
#更多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)
… …
二、跨越式的效能突破:全速前進
而他們在opcode環節後引入了類型推斷(TypeInf),然後透過JIT產生ByteCodes,然後再執行。
於是,在benchmark(測試程式)中得到令人興奮的結果,實現JIT後效能比PHP5.5提升了8倍。然而,當他們把這個優化放入到實際的專案WordPress(一個開源部落格專案)中,卻幾乎看不見效能的提升,得到了一個令人費解的測試結果。
於是,他們使用Linux下的profile類型工具,對程式執行進行CPU耗時佔用分析。
執行100次WordPress的CPU消耗的分佈(截圖來自PPT):
註解:
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如果過大,執行指令數量過多,導致多層快取無法容納如此之多的數據,部分指令將不得不被存放到記憶體區域。
CPU的各級快取的大小也是有限的,下圖是Intel i7 920的設定資訊:
因此,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的Zval,記憶體佔據16個位元組(截圖來自PPT):
Zval從24個位元組下降到16個字節,為什麼會下降呢,這裡需要補一點點的C語言基礎,輔助不熟悉C的同學理解。 struct和union(聯合體)有點不同,Struct的每一個成員變數要各自佔據一塊獨立的記憶體空間,而union裡的成員變數是共用一塊記憶體空間(也就是說修改其中一個成員變量,公有空間就被修改了,其他成員變數的記錄也就沒有了)。因此,雖然成員變數看起來多了不少,但是實際佔據的記憶體空間卻下降了。
除此之外,還有被明顯改變的特性,部分簡單型別不再使用引用。
Zval結構圖(來自PPT):
圖中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(方便成員變數佔位)。
結構體最後一個成員變數採用char數組,而不是使用char*,這裡有一個小最佳化技巧,可以降低CPU的cache miss。
如果使用char數組,當malloc申請上述結構體內存,是申請在同一片區域的,通常是長度是sizeof(_zend_string) 實際char存儲空間。但是,如果使用char*,那個這個位置儲存的只是一個指針,真實的儲存又在另外一片獨立的記憶體區域內。
使用char[1]和char*的記憶體分配比較:
從邏輯實現的角度來看,兩者其實也沒有太大差別,效果很類似。而實際上,當這些記憶體區塊被載入到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):
這個圖看起來很複雜,各種指標跳來跳去,當我們透過key值存取一個元素內容的時候,有時需要3次的指針跳躍才能找對需要的內容。而最重要的一點,就在於這些數組元素存儲,都是分散在各個不同的記憶體區域的。同理可得,在CPU讀取的時候,因為它們就很可能不在同一級快取中,會導致CPU不得不到下級快取甚至記憶體區域查找,也就是造成CPU快取命中下降,進而增加更多的耗時。
PHP7的Zend Array(截圖來自PPT):
新版本的陣列結構,非常簡潔,讓人眼睛一亮。最大的特點是,整塊的陣列元素和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):
圖中,在vm堆疊中的指令send_val和recv參數的指令是相同,PHP7透過減少這兩條重複,來達到函數呼叫機制的底層最佳化。
PHP7的函數呼叫機制(截圖來自於PPT):
#8. 透過巨集定義和內聯函數(inline),讓編譯器提前完成部分工作
C語言的巨集定義會被在預處理階段(編譯階段)執行,提前將部分工作完成,無需在程式執行時分配內存,能夠實現類似函數的功能,卻沒有函數呼叫的壓棧、彈棧開銷,效率會比較高。內聯函數也類似,在預處理階段,將程式中的函數替換為函數體,真實運行的程式執行到這裡,就不會產生函數呼叫的開銷。
PHP7在這方面做了不少的最佳化,將不少需要在執行階段要執行的工作,放到了編譯階段。例如參數類型的判斷(Parameters Parsing),因為這裡涉及的都是固定的字元常數,因此,可以放到編譯階段來完成,進而提升後續的執行效率。
例如處理傳遞參數類型的方式,從左邊的寫法,優化為右邊巨集的寫法。
三、小結
鳥哥的PPT裡放出過一組對比數據,就是WordPress在PHP5.6執行100次會產生70億次的CPU指令執行數目,而在PHP7中只需要25億次,減少64.2%,這是一個令人震撼的數據。
在鳥哥的整個分享中,給我最深刻的一個觀點是:要注意細節,很多細小的優化,一點點持續地積累,積少成多,最終匯聚為驚豔的成果。為山九仞,豈一日之功,我想大概也是這個道理。
毫無疑問,PHP7在效能方面實現跨越式的提升,如果能夠將這些成果應用在PHP的Web系統中,也許我們只需要更少的機器,就可以支撐起更高請求量的服務。 PHP7正式版的發布,令人充滿無限憧憬。
推薦教學:《php影片教學》
以上是學習PHP7的革新與效能最佳化的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

Safe Exam Browser
Safe Exam Browser是一個安全的瀏覽器環境,安全地進行線上考試。該軟體將任何電腦變成一個安全的工作站。它控制對任何實用工具的訪問,並防止學生使用未經授權的資源。

MantisBT
Mantis是一個易於部署的基於Web的缺陷追蹤工具,用於幫助產品缺陷追蹤。它需要PHP、MySQL和一個Web伺服器。請查看我們的演示和託管服務。

SecLists
SecLists是最終安全測試人員的伙伴。它是一個包含各種類型清單的集合,這些清單在安全評估過程中經常使用,而且都在一個地方。 SecLists透過方便地提供安全測試人員可能需要的所有列表,幫助提高安全測試的效率和生產力。清單類型包括使用者名稱、密碼、URL、模糊測試有效載荷、敏感資料模式、Web shell等等。測試人員只需將此儲存庫拉到新的測試機上,他就可以存取所需的每種類型的清單。

ZendStudio 13.5.1 Mac
強大的PHP整合開發環境