PHP 7.4 新特性 —— 預載入

LeoYao發表於2020-07-13

介紹

PHP 已經使用了多年的操作碼快取(APC,Turck MMCache,Zend OpCache)。它們通過 ALMOST 實現了顯著的效能提升,完全消除了 PHP 程式碼重新編譯的開銷。使用操作碼快取,檔案被編譯一次(在第一個使用它們的請求上),然後儲存在共享記憶體中。以下所有 HTTP 請求都使用共享記憶體中快取的表示形式。

這個提議是關於上面提到的 ALMOST。雖然將檔案儲存在操作碼快取中可以消除編譯開銷,但仍然需要從快取中獲取檔案和特定請求的上下文。我們仍然需要檢查原始檔是否被修改,將類和函式的某些部分從共享記憶體快取複製到程式記憶體等。值得注意的是,由於每個 PHP 檔案都是完全獨立於任何其他檔案進行編譯和快取的,因此我們可以當我們將檔案儲存在操作碼快取中時,不解決儲存在不同檔案中的類之間的依賴關係,並且必須在每個請求的執行時重新連結類依賴關係。

該提案的靈感來自為 Java HotSpot VM 設計的“類資料共享”技術。它旨在為使用者提供交易傳統 PHP 模型提供的一些靈活性的能力,以提高效能。在伺服器啟動時,在執行任何應用程式程式碼之前,我們可以將一組PHP檔案載入到記憶體中,並使其內容“永久可用”到該伺服器將服務的所有後續請求。這些檔案中定義的所有函式和類將可用於開箱即用的請求,與內部實體完全相同(例如 strlen()Exception )。通過這種方式,我們可以預載入整個或部分框架,甚至整個應用程式類庫。它還允許引入將用 PHP 編寫的“內建”函式(類似於HHVM 的 sytemlib)。交易靈活性包括一旦伺服器啟動就無法更新這些檔案(更新檔案系統上的這些檔案將不會執行任何操作;將需要重新啟動伺服器以應用更改);而且,這種方法與託管多個應用程式的伺服器或多個版本的應用程式不相容,對於具有相同名稱的某些類具有不同的實現,如果此類從一個應用程式的程式碼庫預載入,則會發生衝突從其他應用程式載入不同的類實現。

提案

只需一個新的 php.ini 指令,opcache.preload 即可控制預載入。使用此指令,我們將指定一個PHP檔案,它將執行預載入任務。載入後,該檔案將完全執行,並可以通過包含它們或使用 opcache_compile_file()函式預載入其他檔案。以前,我嘗試使用豐富的 DSL 來指定要載入哪些檔案,使用模式匹配等來忽略哪些檔案,但後來意識到在 PHP 中編寫預載入方案本身更簡單,更靈活。

例如,以下指令碼引入了一個輔助函式,並使用它來預載入整個 Zend Framework。

<?php 
function _preload($preload , string $pattern  =  "/\ .php $ /", array  $ignore  =  []) { 
  if (is_array($preload)) { 
    foreach ($preload as $path) {_preload($path, $pattern, $ignore); 
    } 
  }  elseif (is_string($preload) { 
    $path = $preload ; 
    if (!in_array($path, $ignore) { 
      if (is_dir($path) { 
        if ($dh = opendir($path)) { 
          while ($file = readdir($dh)) !==  false) { 
            if ($file !== "." &&& $file  !== ".."{
                _preload($path . "/" . $file, $pattern, $ignore); 
            }
          }
          closedir($DH); 
        } 
      }  elseif(is_file($path) && preg_match($pattern, $path)) { 
        if (!opcache_compile_file($path) {
          trigger_error("預載入失敗", E_USER_ERROR); 
        } 
      } 
    } 
  } 
} 

通過set_include_pathget_include_path() 。 PATH_SEPARATOR。 真實路徑(“/無功/網路/ ZendFramework /庫” )); _preload([ “/ var / www / ZendFramework / library” ] );

如上所述,預載入的檔案將永遠快取在opcache記憶體中。如果沒有其他伺服器重新啟動,修改其相應的原始檔將不起任何作用。這些檔案中定義的所有函式和大多數類都將永久載入到PHP的函式和類表中,並在將來的任何請求的上下文中永久可用。在預載入期間,PHP還解析了類依賴關係以及與父,介面和特徵的連結。它還會刪除不必要的包含並執行其他一些優化。

opcache_reset()不會重新載入預載入的檔案。使用當前的opcache設計是不可能的,因為在重啟期間,某些程式可能會使用它們,任何修改都可能導致崩潰。

opcache_get_status 擴充套件為提供有關“preload_statistics”索引下的預載入函式,類和指令碼的資訊。

靜態成員和靜態變數

為了避免誤解,很明顯說預載入不會改變靜態類成員和靜態變數的行為。他們的價值觀不會重新請求邊界。

預載入限制

只能預載入沒有未解析的父,介面,特徵和常量值的類。如果一個類不滿足這個條件,它將作為相應PHP指令碼的一部分以與沒有預載入相同的方式儲存在opcache SHM中。此外,只有未巢狀在控制結構中的頂級實體(例如if()…)可以被預載入。

在Windows上,也無法預載入從內部繼承的類。Windows ASLR和fork()的缺失不允許保證不同程式中內部類的相同地址。

實施細節

預載入是作為 opcache 的一部分在另一個(已經提交的)補丁之上實現的,該補丁引入了“不可變”類和函式。他們假設不可變部分儲存在共享記憶體中一次(對於所有程式)並且從不復制到程式記憶體,但變數部分是特定於每個程式的。該補丁引入了 MAP_PTR 指標資料結構,允許來自SHM的指標處理記憶體。

向後不相容的變化

除非明確使用,否則預載入不會影響任何功能。但是,如果使用它,它可能會破壞某些應用程式行為,因為預載入的類和函式始終可用,並且function_exists()或 class_exists()檢查將返回TRUE,從而阻止執行預期的程式碼路徑。如上所述,具有多個應用程式的伺服器上的錯誤使用也可能導致失敗。由於不同的應用程式(或同一應用程式的不同版本)可能在不同的檔案中具有相同的類/函式名稱,如果預先載入了該類的一個版本 - 它將阻止載入在不同檔案中定義的該類的任何其他版本。

建議的PHP版本

PHP 7.4

RFC影響

對 Opcache

預載入是作為opcache的一部分實現的。

php.ini預設值

  • opcache.preload - 指定將在伺服器啟動時編譯和執行的PHP指令碼。

效能

在沒有任何程式碼修改的情況下使用預載入,我在ZF1_HelloWorld(3620 req / sec vs 2650 req / sec)上獲得30%的加速,在ZF2Test(1300 req / sec vs 670 req / sec)參考應用中獲得50%。但是,實際收益將取決於程式碼的引導開銷與程式碼執行時之間的比率,並且可能會更低。對於具有較短執行時間的請求(例如微服務),這可能會提供最明顯的收益。

未來範圍

  • 預載入可以在 HHVM 中用作 systemlib,以在 PHP 中定義“標準”函式/類

  • 可以預編譯預載入指令碼並使用二進位制格式(甚至可以是原生的 .so.dll )來加速伺服器啟動。

  • ext / FFI(危險擴充套件)一起使用時,我們可能僅在預載入的 PHP 檔案中允許 FFI 功能,但在常規的 PHP 檔案中不允許

  • 可以執行更積極的優化併為預載入的函式和類生成更好的 JIT 程式碼(類似於 HHVM 中的 HHVM Repo 權威模式)

  • 使用某種部署機制擴充套件預載入,更新預載入的bundle而不重啟伺服器會很棒。

參考

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章