在上一篇文章中,我們從 PHP 是解釋性語言、動態語言和底層實現等三個方面,探討了 PHP 性能的問題。本文就深入到 PHP 的微觀層面,我們來了解 PHP 在使用和編寫代碼過程中,性能方面,可能需要注意和提升的地方。
在開始分析之前,我們得掌握一些與性能分析相關的函數。這些函數讓我們對程序性能有更好的分析和評測。
一、性能分析相關的函數與命令
1.1、時間度量函數
平時我們常用 time() 函數,但是返回的是秒數,對於某段代碼的內部性能分析,到秒的精度是不夠的。於是要用 microtime 函數。而 microtime 函數可以返回兩種形式,一是字符串的形式,一是浮點數的形式。不過需要注意的是,在缺省的情況下,返回的精度只有4位小數。為了獲得更高的精確度,我們需要配置 precision。
如下是 microtime 的使用結果。
- $start= microtime(true);
- echo $start."/n";
- $end = microtime(true);
- echo $end."/n";
- echo ($end-$start)."/n";
輸出為:
- bash-3.2# phptime.php
- 1441360050.3286
- 1441360050.3292
- 0.00053000450134277
而在代碼前面加上一行:
- ini_set("precision", 16);
輸出為:
- bash-3.2# phptime.php
- 1441360210.932628
- 1441360210.932831
- 0.0002031326293945312
除了 microtime 內部統計之外, 還可以使用 getrusage 來取得用戶態的時長。在實際的操作中,也常用 time 命令來計算整個程序的運行時長,通過多次運行或者修改代碼後運行,得到不同的時間長度以得到效率上的區別。 具體用法是:time phptime.php ,則在程序運行完成之後,不管是否正常結束退出,都會有相關的統計。
- bash-3.2# time phptime.php
- 1441360373.150756
- 1441360373.150959
- 0.0002031326293945312
- real 0m0.186s
- user 0m0.072s
- sys 0m0.077s
因為本文所討論的性能問題,往往分析上百萬次調用之後的差距與趨勢,為了避免代碼中存在一些時間統計代碼,後面我們使用 time 命令居多。
1.2、內存使用相關函數
分析內存使用的函數有兩個:memory_ get_ usage、memory_ get_ peak_usage,前者可以獲得程序在調用的時間點,即當前所使用的內存,後者可以獲得到目前為止高峰時期所使用的內存。所使用的內存以字節為單位。
- $base_memory= memory_get_usage();
- echo "Hello,world!/n";
- $end_memory= memory_get_usage();
- $peak_memory= memory_get_peak_usage();
- echo $base_memory,"/t",$end_memory,"/t",($end_memory-$base_memory),"/t", $peak_memory,"/n";
輸出如下:
- bash-3.2# phphelloworld.php
- Hello,world!
- 224400 224568 168 227424
可以看到,即使程序中間只輸出了一句話,再加上變量存儲,也消耗了168個字節的內存。
對於同一程序,不同 PHP 版本對內存的使用並不相同,甚至還差別很大。
- $baseMemory= memory_get_usage();
- class User
- {
- private $uid;
- function __construct($uid)
- {
- $this->uid= $uid;
- }
- }
- for($i=0;$i<100000;$i++)
- {
- $obj= new User($i);
- if ( $i% 10000 === 0 )
- {
- echo sprintf( '%6d: ', $i), memory_get_usage(), " bytes/n";
- }
- }
- echo " peak: ",memory_get_peak_usage(true), " bytes/n";
在 PHP 5.2 中,內存使用如下:
- [root@localhostphpperf]# php52 memory.php
- 0: 93784 bytes
- 10000: 93784 bytes
- …… 80000: 93784 bytes
- 90000: 93784 bytes
- peak: 262144 bytes
PHP 5.3 中,內存使用如下
- [root@localhostphpperf]# phpmemory.php
- 0: 634992 bytes
- 10000: 634992 bytes
- …… 80000: 634992 bytes
- 90000: 634992 bytes
- peak: 786432 bytes
可見 PHP 5.3 在內存使用上要粗放了一些。
PHP 5.4 – 5.6 差不多,有所優化:
- [root@localhostphpperf]# php56 memory.php
- 0: 224944 bytes
- 10000: 224920 bytes
- …… 80000: 224920 bytes
- 90000: 224920 bytes
- peak: 262144 bytes
而 PHP 7 在少量使用時,高峰內存的使用,增大很多。
- [root@localhostphpperf]# php7 memory.php
- 0: 353912 bytes
- 10000: 353912 bytes
- …… 80000: 353912 bytes
- 90000: 353912 bytes
- peak: 2097152 bytes
從上面也看到,以上所使用的 PHP 都有比較好的垃圾回收機制,10萬次初始化,並沒有隨著對象初始化的增多而增加內存的使用。PHP7 的高峰內存使用最多,達到了接近 2M。
下面再來看一個例子,在上面的代碼的基礎上,我們加上一行,如下:
$obj->self = $obj;
代碼如下:
- $baseMemory= memory_get_usage();
- class User
- {
- private $uid;
- function __construct($uid)
- {
- $this->uid= $uid;
- }
- }
- for($i=0;$i<100000;$i++)
- {
- $obj= new User($i);
- $obj->self = $obj;
- if ( $i% 5000 === 0 )
- {
- echo sprintf( '%6d: ', $i), memory_get_usage(), " bytes/n";
- }
- }
- echo " peak: ",memory_get_peak_usage(true), " bytes/n";
這時候再來看看內存的使用情況,中間表格主體部分為內存使用量,單位為字節。
圖表如下:
PHP 5.2 並沒有合適的垃圾回收機制,導致內存使用越來越多。而5.3 以後內存回收機制導致內存穩定在一個區間。而也可以看見 PHP7 內存使用最少。把 PHP 5.2 的圖形去掉了之後,對比更為明顯。
可見 PHP7 不僅是在算法效率上,有大幅度的提升,在大批量內存使用上也有大幅度的優化盡管小程序的高峰內存比歷史版本所用內存更多)。
1.3、垃圾回收相關函數
在 PHP 中,內存回收是可以控制的,我們可以顯式地關閉或者打開垃圾回收,一種方法是通過修改配置,zend.enable_gc=Off 就可以關掉垃圾回收。 缺省情況下是 On 的。另外一種手段是通過 gc _enable()和gc _disable()函數分別打開和關閉垃圾回收。
比如在上面的例子的基礎上,我們關閉垃圾回收,就可以得到如下數據表格和圖表。
代碼如下:
- gc_disable();
- $baseMemory= memory_get_usage();
- class User
- {
- private $uid;
- function __construct($uid)
- {
- $this->uid= $uid;
- }
- }
- for($i=0;$i<100000;$i++)
- {
- $obj= new User($i);
- $obj->self = $obj;
- if ( $i% 5000 === 0 )
- {
- echo sprintf( '%6d: ', $i), memory_get_usage(), " bytes/n";
- }
- }
- echo " peak: ",memory_get_peak_usage(true), " bytes/n";
分別在 PHP 5.3、PHP5.4 、PHP5.5、PHP5.6 、PHP7 下運行,得到如下內存使用統計表。
圖表如下,PHP7 還是內存使用效率最優的。
從上面的例子也可以看出來,盡管在第一個例子中,PHP7 的高峰內存使用數是最多的,但是當內存使用得多時,PHP7 的內存優化就體現出來了。
這裡值得一提的是垃圾回收,盡管會使內存減少,但是會導致速度降低,因為垃圾回收也是需要消耗 CPU 等其他系統資源的。Composer 項目就曾經因為在計算依賴前關閉垃圾回收,帶來成倍性能提升,引發廣大網友關注。詳見:
https://github.com/composer/composer/commit/ac676f47f7bbc619678a29deae097b6b0710b799
在常見的代碼和性能分析中,出了以上三類函數之外,還常使用的有堆棧跟蹤函數、輸出函數,這裡不再贅述。