2014年1月20日 星期一

Zend VM: zend object, persistent zval 以及 persistent hash table

趁現在還有印象,筆記一下關於 Zend VM 內部的 persistency mechanism:

1. ALLOC_INIT_ZVAL 基本上就是 ALLOC_ZVAL 加上 INIT_ZVAL

2. MAKE_STD_ZVALALLOC_ZVAL 加上 INIT_PZVAL

3. INIT_PZVALINIT_ZVAL 的差異是 PZVAL 會將 refcount__gc 設置為 1 而 INIT_ZVAL 基本上只是將所有的欄位設置為 0。 所以 ALLOC_INIT_ZVAL 出來的 zval 如果沒有處理好,會被 gc 清掉。

4. zend object 不支援 persistent,其中一部分原因在於 object 基本上分為 zend_object 以及 zend_object_value, zend_object 是 persistent,但 zend_object_value 不是,且 object 本身的 properties 跟 handlers 都是使用 emalloc 申請來的,這些記憶體會在 request 結束的時候一起 free 掉。

5. HashTable 結構可以 allocate 成 persistent memory,其中幾例是 EG(persistent_list)EG(symbol_table). 基本上需先使用 pemalloc allocate persistent memory 再使用 zend_hash_init 初始化 HashTable 結構變數,其中最後一項參數 persistent 必須為 1,則 allocate bucket 時,bucket 本身的記憶體才會是 persistent。

6. zend_hash_copy 基本上是直接對 buckets 做拷貝的動作,所以 zval* 實際上還是同一個,只是在 copy_ctor_func 你可以透過 Z_ADDREF_P 來增加 reference counting。

7. 要對 HashTable 完全拷貝,只能另外自己寫 recursive copy function,對於 value 內的 zval* 得另外 allocate zval* 出來,這樣在 persistent_list 內才不會被 request 結束時的 clean up handler 清除掉。

8. 也就是說 object 沒有辦法 persistent 保留在程序中,在 APC 的作法,則是針對 object 去做 serialize。 沒錯就是那個 serialize 函數。

9. zval 基本上都是 copy on write,如果在 extension 內要明確的複製 zval 結構到新的記憶體空間,則必須使用 SEPARATE_ZVAL

10. 只要是 emalloc 來的記憶體,不管 refcount 是否大於 0,"幾乎" 都會在 RSHUTDOWN 時被清除掉。

2014年1月7日 星期二

Pux - 以新的概念重新設計 PHP Router

時下開源的 PHP Router 如 Symfony/Routing 或 Zend\Mvc\Router 多半是以一個 Persistent  HTTP server 的角度來設計,然而一般 PHP 是通過 CGI 或 Apache 來執行,這樣的設計會讓您的 PHP 應用程式在執行時間跟編譯時間消耗太多效能,有過多的方法呼叫、類別載入、物件建置等等。

在這些舊有的模式下,每一個新的 HTTP 請求進來,PHP 就必須重新把每個模組的路徑載入,且透過多個 method call 把路徑定義到一個 PHP Array,再使用 PHP 去對每個路徑做比對。

有些 Router 的設計甚至做了更多的預先處理,譬如: Symfony/Routing 得利用 RouteCompiler 把路徑編譯成 PCRE Pattern, Symfony/Routing 甚至強迫每個 Route 都一定得使用 PCRE 來比對。

雖然在小型應用程式還過得去,但在稍微大一點的 PHP 應用程式,Controller 與 Route Path 動扎幾十幾百個,整體消耗下來的多餘計算其實相當可觀。

這些重複的預先處理其實是不必要的,如果能夠避免預先處理或者函數呼叫,就可以提昇整體效能。

然而,直接定義 Route 成 PHP Array 雖然可以增加效能,但反而卻增加了開發成本。



因此,針對這些問題,筆者在去年底跨年夜,重新設計了一個新的 PHP Router。


Pux (http://c9s.github.io/Pux/) 是一個以效能為導向所設計的 PHP Router,針對 PHP 本身在 CGI 或 Apache 環境上的執行時間 (Run-time) 特性,以新的方式設計的 PHP Router。

Pux 簡化了每個路徑的資料結構,並自動將路徑類型分為兩種類型,一種是需透過 PCRE 正規表示來比對的路徑,另外一種則是純字串的靜態路徑。

會這麼做的原因,是因為 PCRE 正規表示的比對相對比純字串比對慢,甚至靜態路徑可以直接透過 Hash 表來查找。

此外 Pux 針對在 Production 環境上效能重點,支持了使用 C 語言開發的 PHP Extension,只要安裝這個 Extension,就可以避免 PHP Class 重新載入,路徑比對的速度也會更快,可得到更高的執行效能。


使用


使用 Pux,您可在任何一個 framework 內建置您的 Route 定義檔,在檔案最尾端回傳 Mux 物件

// load your composer autoload if it's needed
// require '../vendor/autoload.php';
use Pux\Mux;
$mux = new Mux;
$mux->get('/hello', ['HelloController','helloAction']);
return $mux;
接著使用 Pux 提供的命令列工具將定義檔編譯成 PHP Array:

pux compile -o hello_mux.php hello_routes.php
接著在您的應用程式內,只要寫一行 require 引入這個檔案就可以直接使用 Mux 做路徑比對的動作:

$mux = require "hello_mux.php";
$route = $mux->dispatch('/hello');

效能


以重新設計過的 Pux Router (Phux 舊名),在 iMac 2012 Mid 機器上 (Rough Benchmark) 與 Symfony/Routing 的效能比較,以下是參考數據:




反應時間的部份, Pux 純 PHP 版本平均需要 8-10ms 的反應時間,但 Symfony 最 Minimal 的載入至少需要 9ms 平均 30-40ms 左右

Pux - Requests per second
Symfony/Routing - Requests per second


測試案例的程式碼可在 router-benchmark 找到,兩者的測試案例都相當簡單,只有一個 /hello route ,並且純粹就 dispatch 的速度來做比較。

註: Symfony/Routing 的測試案例不包含 Controller, Symfony, UrlGenerator, Apache2 Rule Dumper。

Testing with route dispatch only. (no controller)

Hardware:
  • iMac Mid 2011
  • Processor 2.5 GHz Intel Core i5
  • Memory 12 GB 1333 MHz DDR3
  • Software OS X 10.9.1 (13B42)
Environment:
  • Apache 2.2 + prefork worker
  • PHP 5.5.6 + opcache