譯者 | 李睿
審校 | 重樓
PHP引擎生成的PHP操作碼深受編寫代碼方式的影響,這不僅僅體現在完成任務所需的語句數量上。其次,代碼的語法可能完全改變生成的操作碼,從而導致服務器的CPU在執行完全相同的代碼時會產生大量開銷。
在過去的幾年,SaaS產品有了巨大的增長,提供了越來越深入優化技術的機會,以盡可能高效地運行工作負載。得出的結果令人印象深刻,可以幫助開發人員釋放現金流,以繼續SaaS之旅。
在目前SaaS產品的案例中,其PHP進程每天在一臺擁有2個vCPU和8GB內存的服務器上處理12億個以上數據包。并且使用AWS自動擴展組,以便在不可預測的峰值情況下具有更大的靈活性,但很少使用第二臺服務器(每周一兩次)。
什么是PHP操作碼?
PHP操作碼指的是PHP引擎在編譯完開發人員編寫的PHP源代碼后執行的低級指令。
在PHP中,代碼編譯是在運行時(runtime)發生的:基本上,當PHP引擎首次獲取代碼時,它會將其編譯成這種機器友好的代碼,緩存(這樣引擎就不會再次編譯相同的代碼),然后執行。
下圖是這個過程的簡單表示:
PHP操作碼緩存
緩存PHP操作碼允許開發人員在執行代碼的過程中節省三個步驟:解析原始PHP代碼、標記化和編譯。
一旦為代碼首次生成了操作碼,它就會存儲在內存中,以便在后續請求中重復使用。這減少了PHP引擎每次執行相同PHP代碼時都需要重新編譯的需求,從而節省了大量CPU和內存的開銷。
PHP中最常用的操作碼緩存是OPCache,從PHP 5.5到最近的版本默認包含了OPCache。其效率高,并得到了廣泛支持。
緩存預編譯的腳本字節碼需要在每次部署后使緩存失效。這是因為,如果更改后的文件在緩存中有字節碼版本,PHP將繼續運行舊版本的代碼,直到清除操作碼緩存,因此將再次編譯新代碼,生成新的緩存項。
如何調查PHP操作碼
為了理解不同的語法如何影響腳本的操作碼,需要獲取PHP引擎生成的編譯代碼的方法。
有兩種方法可以獲得操作碼。
OPCache原生函數
如果在服務器上啟用了OPCache擴展,則可以使用其原生函數獲取特定PHP文件的操作碼:
PHP
// Force compilation of a script
opcache_compile_file(__DIR__.'/yourscript.php');
// Get OPcache status
$status = opcache_get_status();
// Inspect the script's entry in the cache
print_r($status['scripts'][__DIR__.'/yourscript.php']);
VLD(Vulcan邏輯反匯編器)PHP擴展
VLD是一個流行的PHP擴展,它可以反匯編編譯后的PHP代碼并輸出操作碼。它是了解PHP如何解釋和執行代碼的強大工具。在安裝之后,可以使用帶-d選項的PHP命令運行一個啟用VLD的PHP腳本:
Shell
php -d vld.active=1 -d vld.execute=0 yourscript.php
輸出將包括有關編譯后的操作碼的詳細信息,包括每個操作及其相關的代碼行等等。
使用3v4l(EVAL的縮寫)
3v4l是一個非常有用的在線工具,它允許開發人員查看其在編輯器中輸入的PHP代碼生成的操作碼。它基本上是一個安裝了VLD的PHP服務器,因此它可以獲取VLD輸出并在瀏覽器中顯示操作碼。
由于它是免費的,將在以下的分析中使用這個在線工具。
如何生成高效的PHP操作碼
3v4l非常適合理解使用的代碼語法如何以好或壞的方式影響生成的PHP操作碼。以下開始將下面的代碼粘貼到3v4l中。保持配置為“所有支持的版本”,然后單擊“eval”。
PHP
<?php
namespace App;
strlen('ciao');
在執行代碼后,底部將出現一個選項卡菜單。導航到VLD選項卡以可視化相應的操作碼。
Shell
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
5 0 E > INIT_NS_FCALL_BY_NAME 'App%5CSpace%5Cstrlen'
1 SEND_VAL_EX 'ciao'
2 DO_FCALL 0
3 > RETURN 1
需要注意的是,第一個操作是INIT_NS_FCALL_BY_NAME。解釋器使用當前文件的名稱空間構造函數的名稱,但在 App\Example 命名空間中并不存在這個函數——那么它是如何工作的呢?
解釋器將檢查該函數是否存在于當前命名空間中。如果不存在,它會嘗試調用相應的核心函數。
在這里有機會告訴解釋器避免這種雙重檢查,并直接執行核心函數。
嘗試在strlen之前添加反斜杠(\),并單擊“eval”:
PHP
<?php
namespace App;
\strlen('ciao');
在VLD選項卡中,現在只需一條語句即可看到操作碼。
line #* E I O op fetch ext return operands
------------------------------------------------------------------------------------- 5 0 E > > RETURN 1
因為傳達了函數的確切位置,所以不需要考慮任何回退。
如果不喜歡使用反斜杠,可以像從根命名空間導入其他類一樣導入該函數:
PHP
<?php
namespace App;
use function strlen;
strlen('ciao');
利用自動操作碼優化
PHP引擎還有很多內部自動化功能,可以提前生成優化的操作碼,對靜態表達式進行求值。這是PHP自7.x版本以來性能顯著提高的最重要原因之一。
了解這些動態可以真正減少資源消耗并降低成本。在進行這項研究之后,已經開始在整個代碼中使用這些技巧。
以下展示一個使用PHP常量的示例。在3v4l中運行這個腳本:
PHP
<?php
namespace App;
if (PHP_OS === 'Linux') {
echo "Linux";
}
查看PHP操作碼的前兩行內容:
line #* E I O op fetch ext return operands
------------------------------------------------------------------------------------- 5 0 E > FETCH_CONSTANT ~0 'App%5CPHP_OS' 1 IS_IDENTICAL ~0, 'Linux' 2 > JMPZ ~1, ->4 6 3 > ECHO 'Linux' 7 4 > > RETURN 1
FETCH_CONSTANT嘗試從當前名稱空間獲取PHP_OS的值,它將查找全局名稱空間,因為它在此處不存在。然后,IS_IDENTICAL指令執行IF語句。
現在嘗試將反斜杠添加到常量中:
PHP
<?php
namespace App;
if (\PHP_OS === 'Linux') {
echo "Linux";
}
正如在操作碼中看到的那樣,引擎不需要嘗試獲取常量,因為其位置現已明確,并且作為一個靜態值,它已經被存儲在內存中。
此外,IF語句消失了,因為IS_IDENTITCAL語句的另一端是一個靜態字符串('Linux'),因此IF可以標記為“true”,而無需在每次執行時解釋它。
這凸顯了開發人員在PHP代碼的最終性能上可以產生的重要影響。
結論
希望這是一個有趣的話題。正如在文章開頭提到的那樣,開發人員通過使用這種策略獲得了很多好處,事實上,它們也在軟件包中使用。
可以在這里看到一個示例,說明如何在PHP包中使用這些技巧來優化其性能。
原文標題:PHP Opcode: Improve Application Performance Without Changing Your Code,作者:Valerio Barbera