在開發過程中發現,用require來載入一個很大(幾百K,甚至幾兆)的設定檔時,會造成響應逾時。如果把這個設定檔的內容序列化後,用file_get_contents擷取檔案然後還原序列化的方法來載入,就會快很多。
經過近兩周的研究,大概知道了其中的原因。
首先,還從PHP的流程說起,PHP其實有兩個流程,一個是啟動的流程,一個是響應請求的流程。PHP作為Apache的一個模組,向Apache註冊了兩個函數,一個是Aapche啟動的時候啟動並執行函數:sapi_startup;一個是Apache接收到請求的時候調用的函數:php_handler
啟動的流程:
Apache啟動
-> sapi_startup
-> php_module_startup (PHP啟動總開關)
-> zend_startup (啟動Zend引擎,包括初始化全域變數,初始化 compile 和 execute 函數
相應請求的流程:
Apache收到請求
-> sapi_startup
-> zend_activate (包括初始化編譯器、初始化執行器、啟動掃描器)
-> zend_compiler (文法分析、語意分析、產生opcode)
-> zend_execute (執行每個opcode)
-> zend_deactive(清理本次請求用到的資料)
如果遇到 require 或者 include 之類的函數時,會 從 zend_execute 階段重新回到 zend_compiler 階段,開始解釋PHP,執行PHP的過程。
除了 zend_compiler 和 zend_execute 階段之外,require 和 file_get_contents 的開銷基本是一樣的。
而且我們伺服器上安裝了apc擴充,就是說 zend_compiler 階段可以認為兩者也是一樣的。
那他們的效能九差在zend_execute階段了。
首先,讓我們用vld擴充查看一下兩個檔案產生的opcode的數量,因為這個是execute的輸入。
結果顯示,require 產生的opcode數量為2萬多個,大多是 ADD_ARRAY_ELEMENT,就是構造資料;而file_get_contents產生的opcode只有6個;
然後再來對比執行的效率:
這兩個函數的執行可以分成兩部分:讀取檔案和構造設定檔裡面的數組;
先說讀取檔案,require讀取的機制是,以8192位元組大小的buffer迴圈將檔案讀入記憶體;而file_get_contents使用的是mmap,直接將檔案對應到了虛擬記憶體當中。這樣的話,require會比file_get_contents多出大量的系統調用。而file_get_contents無需作這麼多使用者態和核心態的切換工作。這一步,file_get_contents勝出一籌;
再來看構造數組,require構造的機制是產生2萬多個opcode,然後一次執行這些opcode;而file_get_contents使用的是unserialize函數,他對傳入的文本進行解析,然後逐級構造成數組。他們構造數組的思路是一樣的,但是require每增加一級資料的開銷要比unserialize大;這一局也是 file_get_contents 略優;
但是,file_get_contents 在PHP內部是函數調用,而require是一個內建的opcode,所以調用file_get_contents時的開銷要比require略大;
所以,小檔案的時候,file_get_contents 讀取檔案時 記憶體映射的優勢發揮不出來,兩者部分伯仲;大檔案的時候,由於require要2K2K的迴圈調用read系統調用,就降低了他的效能。