PHP 效能分析與實驗:效能的微觀分析

oneapm發表於2015-09-13

上一篇文章中,我們從 PHP 是解釋性語言、動態語言和底層實現等三個方面,探討了 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 效能分析與實驗:效能的微觀分析

圖表如下:

PHP 效能分析與實驗:效能的微觀分析

PHP 5.2 並沒有合適的垃圾回收機制,導致記憶體使用越來越多。而5.3 以後記憶體回收機制導致記憶體穩定在一個區間。而也可以看見 PHP7 記憶體使用最少。把 PHP 5.2 的圖形去掉了之後,對比更為明顯。

PHP 效能分析與實驗:效能的微觀分析

可見 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 下執行,得到如下記憶體使用統計表。

PHP 效能分析與實驗:效能的微觀分析

圖表如下,PHP7 還是記憶體使用效率最優的。

PHP 效能分析與實驗:效能的微觀分析

從上面的例子也可以看出來,儘管在第一個例子中,PHP7 的高峰記憶體使用數是最多的,但是當記憶體使用得多時,PHP7 的記憶體優化就體現出來了。

這裡值得一提的是垃圾回收,儘管會使記憶體減少,但是會導致速度降低,因為垃圾回收也是需要消耗 CPU 等其他系統資源的。Composer 專案就曾經因為在計算依賴前關閉垃圾回收,帶來成倍效能提升,引發廣大網友關注。詳見:

https://github.com/composer/composer/commit/ac676f47f7bbc619678a29deae097b6b0710b799

在常見的程式碼和效能分析中,出了以上三類函式之外,還常使用的有堆疊跟蹤函式、輸出函式,這裡不再贅述。

二、PHP 效能分析10則

下面我們根據小程式來驗證一些常見的效能差別。

2.1、使用 echo 還是 print

在有的建議規則中,會建議使用 echo ,而不使用 print。說 print 是函式,而 echo 是語法結構。實際上並不是如此,print 也是語法結構,類似的語法結構,還有多個,比如 list、isset、require 等。不過對於 PHP 7 以下 PHP 版本而言,兩者確實有效能上的差別。如下兩份程式碼:

for($i=0; $i<1000000; $i++)
{
echo("Hello,World!");
}
for($i=0; $i<1000000; $i++)
{
print ("Hello,World!");
}

在 PHP 5.3 中執行速度分別如下(各2次):

[root@localhostphpperf]# time php echo1.php > /dev/null
real 0m0.233s 
user 0m0.153s 
sys 0m0.080s 
[root@localhostphpperf]# time php echo1.php > /dev/null
real 0m0.234s 
user 0m0.159s 
sys 0m0.073s 
[root@localhostphpperf]# time phpecho.php> /dev/null
real 0m0.203s 
user 0m0.130s 
sys 0m0.072s 
[root@localhostphpperf]# time phpecho.php> /dev/null
real 0m0.203s 
user 0m0.128s 
sys 0m0.075s

在 PHP5.3 版中效率差距10%以上。而在 PHP5.4 以上的版本中,區別不大,如下是 PHP7 中的執行效率。

[root@localhostphpperf]# time php7 echo.php> /dev/null
real 0m0.151s 
user 0m0.088s 
sys 0m0.062s 
[root@localhostphpperf]# time php7 echo.php> /dev/null
real 0m0.145s 
user 0m0.084s 
sys 0m0.061s
[root@localhostphpperf]# time php7 echo1.php > /dev/null
real 0m0.140s 
user 0m0.075s 
sys 0m0.064s 
[root@localhostphpperf]# time php7 echo1.php > /dev/null
real 0m0.146s 
user 0m0.077s 
sys 0m0.069s

正如瀏覽器前端的一些優化準則一樣,沒有啥特別通用的原則,往往根據不同的情況和版本,規則也會存在不同。

2.2、require 還是 require_once?

在一些常規的優化規則中,會提到,建議使用 require_ once 而不是 require,現由是 require_ once 會去檢測是否重複,而 require 則不需要重複檢測。

在大量不同檔案的包含中,require_ once 略慢於 require。但是 require_ once 的檢測是一項記憶體中的行為,也就是說即使有數個需要載入的檔案,檢測也只是記憶體中的比較。而 require 的每次重新載入,都會從檔案系統中去讀取分析。因而 require_ once 會比 require 更佳。我們們也使用一個例子來看一下。

str.php
global$str;
$str= "China has a large population";
require.php
for($i=0; $i<100000; $i++) {
require "str.php";
}
require_once.php
for($i=0; $i<100000; $i++) {
require_once"str.php";
}

上面的例子,在 PHP7 中,require_ once.php 的執行速度是 require.php 的30倍!在其他版本也能得到大致相同的結果。

[root@localhostphpperf]# time php7 require.php
real 0m1.712s 
user 0m1.126s 
sys 0m0.569s 
[root@localhostphpperf]# time php7 require.php
real 0m1.640s 
user 0m1.113s 
sys 0m0.515s 
[root@localhostphpperf]# time php7 require_once.php
real 0m0.066s 
user 0m0.063s 
sys 0m0.003s 
[root@localhostphpperf]# time php7 require_once.php
real 0m0.057s 
user 0m0.052s 
sys 0m0.004s

從上可以看到,如果存在大量的重複載入的話,require_ once 明顯優於 require,因為重複的檔案不再有 IO 操作。即使不是大量重複的載入,也建議使用 require_ once,因為在一個程式中,一般不會存在數以千百計的檔案包含,100次記憶體比較的速度差距,一個檔案包含就相當了。

2.3、單引號還是雙引號?

單引號,還是雙引號,是一個問題。一般的建議是能使用單引號的地方,就不要使用雙引號,因為字串中的單引號,不會引起解析,從而效率更高。那來看一下實際的差別。

classUser
{
private $uid;
private $username;
private $age;
function  __construct($uid, $username,$age){
$this->uid= $uid;
$this->username = $username;
$this->age = $age;
    }
function getUserInfo()
    {
return "UID:".$this->uid." UserName:".$this->username." Age:".$this->age;
    }
function getUserInfoSingle()
    {
return 'UID:'.$this->uid.' UserName:'.$this->username.' Age'.$this->age;
    }
function getUserInfoOnce()
    {
return "UID:{$this->uid}UserName:{$this->username} Age:{$this->age}";
    }
function getUserInfoSingle2()
    {
return 'UID:{$this->uid} UserName:{$this->username} Age:{$this->age}';
    }
}
for($i=0; $i<1000000;$i++) {
$user = new User($i, "name".$i, $i%100);
$user->getUserInfoSingle();
}

在上面的 User 類中,有四個不同的方法,完成一樣的功能,就是拼接資訊返回,看看這四個不同的方法的區別。

第一個、getUserInfo ,使用雙引號和屬性相拼接

[root@localhostphpperf]# time php7 string.php
real 0m0.670s 
user 0m0.665s 
sys 0m0.002s 
[root@localhostphpperf]# time php7 string.php
real 0m0.692s 
user 0m0.689s 
sys 0m0.002s 
[root@localhostphpperf]# time php7 string.php
real 0m0.683s 
user 0m0.672s 
sys 0m0.004s

第二個、getUserInfoSingle ,使用單引號和屬性相拼接

[root@localhostphpperf]# time php7 string.php
real 0m0.686s 
user 0m0.683s 
sys 0m0.001s 
[root@localhostphpperf]# time php7 string.php
real 0m0.671s 
user 0m0.666s 
sys 0m0.003s 
[root@localhostphpperf]# time php7 string.php
real 0m0.669s 
user 0m0.666s 
sys 0m0.002s

可見在拼接中,單雙引號並無明顯差別。

第三個、getUserInfoOnce,不再使用句號.連線,而是直接引入在字串中解析。

[root@localhostphpperf]# time php7 string.php
real 0m0.564s 
user 0m0.556s 
sys 0m0.006s 
[root@localhostphpperf]# time php7 string.php
real 0m0.592s 
user 0m0.587s 
sys 0m0.004s 
[root@localhostphpperf]# time php7 string.php
real 0m0.563s 
user 0m0.559s 
sys 0m0.003s

從上面可見,速度提高了0.06s-0.10s,有10%-20%的效率提升。可見連綴效率更低一些。

第四個、getUserInfoSingle2 雖然沒有達到我們真正想要的效果,功能是不正確的,但是在字串中,不再需要解析變數和獲取變數值,所以效率確實有大幅度提升。

[root@localhostphpperf]# time php7 string.php
real 0m0.379s 
user 0m0.375s 
sys 0m0.003s 
[root@localhostphpperf]# time php7 string.php
real 0m0.399s 
user 0m0.394s 
sys 0m0.003s 
[root@localhostphpperf]# time php7 string.php
real 0m0.377s 
user 0m0.371s 
sys 0m0.004s

效率確實有了大的提升,快了50%。

那麼這個快,是由於不需要變數引用解析帶來的,還是隻要加入$天然的呢?我們再試著寫了一個方法。

functiongetUserInfoSingle3()
{
return "UID:{\$this->uid} UserName:{\$this->username} Age:{\$this->age}";
}

得到如下執行時間:

[root@localhostphpperf]# time php7 string.php
real 0m0.385s 
user 0m0.381s 
sys 0m0.002s 
[root@localhostphpperf]# time php7 string.php
real 0m0.382s 
user 0m0.380s 
sys 0m0.002s 
[root@localhostphpperf]# time php7 string.php
real 0m0.386s 
user 0m0.380s 
sys 0m0.004s

發現轉義後的字串,效率跟單引號是一致的,從這裡也可以看見,單引號還是雙引號包含,如果不存在需要解析的變數,幾乎沒有差別。如果有需要解析的變數,你也不能光用單引號,要麼使用單引號和連綴,要麼使用內部插值,所以在這條規則上,不用太過糾結。

2.4、錯誤應該開啟還是關閉?

在 PHP 中,有多種錯誤訊息,錯誤訊息的開啟是否會帶來效能上的影響呢?從直覺覺得,由於錯誤訊息,本身會涉及到 IO 輸出,無論是輸出到終端或者 error_log,都是如此,所以肯定會影響效能。我們來看看這個影響有多大。

error_reporting(E_ERROR);
for($i=0; $i<1000000;$i++) {
$str= "通常,$PHP中的垃圾回收機制,僅僅在迴圈回收演算法確實執行時會有時間消耗上的增加。但是在平常的(更小的)指令碼中應根本就沒有效能影響。
然而,在平常指令碼中有迴圈回收機制執行的情況下,記憶體的節省將允許更多這種指令碼同時執行在你的伺服器上。因為總共使用的記憶體沒達到上限。";
}

在上面的程式碼中,我們涉及到一個不存在的變數,所以會報出 Notice 錯誤:

Notice: Undefined variable: PHP 中的垃圾回收機制,僅僅在迴圈回收演算法確實執行時會有時間消耗上的增加。但是在平常的 in xxxx/string2.php on line 10

如果把 E_ ERROR 改成 E_ ALL 就能看到大量的上述錯誤輸出。

我們先執行 E_ ERROR 版,這個時候沒有任何錯誤日誌輸出。得到如下資料:

[root@localhostphpperf]# time php7 string2.php
real 0m0.442s 
user 0m0.434s 
sys 0m0.005s 
[root@localhostphpperf]# time php7 string2.php
real 0m0.487s 
user 0m0.484s 
sys 0m0.002s 
[root@localhostphpperf]# time php7 string2.php
real 0m0.476s 
user 0m0.471s 
sys 0m0.003s

再執行 E_ ALL 版,有大量的錯誤日誌輸出,我們把輸出重定向到/dev/null

[root@localhostphpperf]# time php7 string2.php > /dev/null
real 0m0.928s 
user 0m0.873s 
sys 0m0.051s 
[root@localhostphpperf]# time php7 string2.php > /dev/null
real 0m0.984s 
user 0m0.917s 
sys 0m0.064s 
[root@localhostphpperf]# time php7 string2.php > /dev/null
real 0m0.945s 
user 0m0.887s 
sys 0m0.056s

可見慢了將近一倍。

如上可見,即使輸出沒有正式寫入檔案,錯誤級別開啟的影響也是巨大的。線上上我們應該將錯誤級別調到 E_ ERROR 這個級別,同時將錯誤寫入 error_ log,既減少了不必要的錯誤資訊輸出,又避免洩漏路徑等資訊,造成安全隱患。

2.5、正規表示式和普通字串操作

在字串操作中,有一條常見的規則,即是能使用普通字串操作方法替代的,就不要使用正規表示式來處理,用 C 語言操作 PCRE 做過正規表示式處理的童鞋應該清楚,需要先 compile,再 exec,也就是說是一個相對複雜的過程。現在就比較一下兩者的差別。

對於簡單的分隔,我們可以使用 explode 來實現,也可以使用正規表示式,比如下面的例子:

ini_set("precision", 16);
function microtime_ex()
{
list($usec, $sec) = explode(" ", microtime());
return $sec+$usec;
}
for($i=0; $i<1000000; $i++) {
microtime_ex();
}

耗時在0.93-1S之間。

[root@localhostphpperf]# time php7 pregstring.php
real 0m0.941s 
user 0m0.931s 
sys 0m0.007s 
[root@localhostphpperf]# time php7 pregstring.php
real 0m0.986s 
user 0m0.980s 
sys 0m0.004s 
[root@localhostphpperf]# time php7 pregstring.php
real 0m1.004s 
user 0m0.998s 
sys 0m0.003s

我們再將分隔語句替換成:

list($usec, $sec) = preg_split("#\s#", microtime());

得到如下資料,慢了近10-20%。

[root@localhostphpperf]# time php7 pregstring1.php
real 0m1.195s 
user 0m1.182s 
sys 0m0.004s 
[root@localhostphpperf]# time php7 pregstring1.php
real 0m1.222s 
user 0m1.217s 
sys 0m0.003s 
[root@localhostphpperf]# time php7 pregstring1.php
real 0m1.101s 
user 0m1.091s 
sys 0m0.005s

再將語句替換成:

list($usec, $sec) = preg_split("#\s+#", microtime());

即匹配一到多個空格,並沒有太多的影響。除了分隔外,查詢我們也來看一個例子。

第一段程式碼:

$str= "China has a Large population";
for($i=0; $i<1000000; $i++) {
if(preg_match("#l#i", $str))
    {
    }
}

第二段程式碼:

$str= "China has a large population";
for($i=0; $i<1000000; $i++) {
if(stripos($str, "l")!==false)
    {
    }
}

這兩段程式碼達到的效果相同,都是查詢字串中有無 l 或者 L 字元。

在 PHP 7 下執行效果如下:

[root@localhostphpperf]# time php7 pregstring2.php
real 0m0.172s 
user 0m0.167s 
sys 0m0.003s 
[root@localhostphpperf]# time php7 pregstring2.php
real 0m0.199s 
user 0m0.196s 
sys 0m0.002s 
[root@localhostphpperf]# time php7 pregstring3.php
real 0m0.185s 
user 0m0.182s 
sys 0m0.003s 
[root@localhostphpperf]# time php7 pregstring3.php
real 0m0.184s 
user 0m0.181s 
sys 0m0.003s

兩者區別不大。再看看在 PHP5.6 中的表現。

[root@localhostphpperf]# time php56 pregstring2.php
real 0m0.470s 
user 0m0.456s 
sys 0m0.004s 
[root@localhostphpperf]# time php56 pregstring2.php
real 0m0.506s 
user 0m0.500s 
sys 0m0.005s 
[root@localhostphpperf]# time php56 pregstring3.php
real 0m0.348s 
user 0m0.342s 
sys 0m0.004s 
[root@localhostphpperf]# time php56 pregstring3.php
real 0m0.376s 
user 0m0.364s 
sys 0m0.003s

可見在 PHP 5.6 中表現還是非常明顯的,使用正規表示式慢了20%。PHP7 難道是對已使用過的正規表示式做了快取?我們調整一下程式碼如下:

$str= "China has a Large population";
for($i=0; $i<1000000; $i++) {
$pattern = "#".chr(ord('a')+$i%26)."#i";
if($ret = preg_match($pattern, $str)!==false)
    {
    }
}

這是一個動態編譯的 pattern。

$str= "China has a large population";
for($i=0; $i<1000000; $i++) {
$pattern = "".chr(ord('a')+$i%26)."";
if($ret = stripos($str, $pattern)!==false)
    {
    }
}

在 PHP7 中,得到了如下結果:

[root@localhostphpperf]# time php7 pregstring2.php
real 0m0.351s 
user 0m0.346s 
sys 0m0.004s 
[root@localhostphpperf]# time php7 pregstring2.php
real 0m0.359s 
user 0m0.352s 
sys 0m0.004s 
[root@localhostphpperf]# time php7 pregstring3.php
real 0m0.375s 
user 0m0.369s 
sys 0m0.003s 
[root@localhostphpperf]# time php7 pregstring3.php
real 0m0.370s 
user 0m0.365s 
sys 0m0.005s

可見兩者並不明顯。而在 PHP 5.6 中,同樣的程式碼:

[root@localhostphpperf]# time php56 pregstring2.php
real 0m1.022s 
user 0m1.015s 
sys 0m0.005s 
[root@localhostphpperf]# time php56 pregstring2.php
real 0m1.049s 
user 0m1.041s 
sys 0m0.005s 
[root@localhostphpperf]# time php56 pregstring3.php
real 0m0.923s 
user 0m0.821s 
sys 0m0.002s 
[root@localhostphpperf]# time php56 pregstring3.php
real 0m0.838s 
user 0m0.831s 
sys 0m0.004s

在 PHP 5.6 中,stripos 版明顯要快於正規表示式版,由上兩例可見,PHP7對正規表示式的優化還是相當驚人的。其次也建議,能用普通字串操作的地方,可以避免使用正規表示式。因為在其他版本中,這個規則還是適用的。某 zend 大牛官方的分享給出如下資料:

  • stripos(‘http://’, $website) 速度是preg_match(‘/http:\/\//i’, $website) 的兩倍
  • ctype_alnum()速度是preg_match(‘/^\s*$/’)的5倍;
  • “if ($test == (int)$test)” 比 preg_match(‘/^\d*$/’)快5倍

可以相見,正規表示式是相對低效的。

2.6、陣列元素定位查詢

在陣列元素的查詢中,有一個關鍵的注意點就是陣列值和鍵的查詢速度,差異非常大。瞭解過 PHP 擴充套件開發的朋友,應該清楚,陣列在底層其實是 Hash 表。所以鍵是以快速定位的,而值卻未必。下面來看例子。

首先們構造一個陣列:

$a= array();
for($i=0;$i<100000;$i++){
$a[$i] = $i;
}

在這個陣列中,我們測試查詢值和查詢鍵的效率差別。

第一種方法用 array_ search,第二種用 array_ key_ exists,第三種用 isset 語法結構。 程式碼分別如下:

//查詢值
foreach($a as $i)
{
array_search($i, $a);
}
//查詢鍵
foreach($a as $i)
{
array_key_exists($i, $a);
}
//判定鍵是否存在
foreach($a as $i)
{
if(isset($a[$i]));
}

執行結果如下:

[root@localhostphpperf]# time php7 array.php
real 0m9.026s 
user 0m8.965s 
sys 0m0.007s 
[root@localhostphpperf]# time php7 array.php
real 0m9.063s 
user 0m8.965s 
sys 0m0.005s 
[root@localhostphpperf]# time php7 array1.php
real 0m0.018s 
user 0m0.016s 
sys 0m0.001s 
[root@localhostphpperf]# time php7 array1.php
real 0m0.021s 
user 0m0.015s 
sys 0m0.004s 
[root@localhostphpperf]# time php7 array2.php
real 0m0.020s 
user 0m0.014s 
sys 0m0.006s 
[root@localhostphpperf]# time php7 array2.php
real 0m0.016s 
user 0m0.009s 
sys 0m0.006s

由上例子可見,鍵值查詢的速度比值查詢的速度有百倍以上的效率差別。因而如果能用鍵值定位的地方,儘量用鍵值定位,而不是值查詢。

2.7、物件與陣列

在 PHP 中,陣列就是字典,字典可以儲存屬性和屬性值,而且無論是鍵還是值,都不要求資料型別統一,所以物件資料儲存,既能用物件資料結構的屬性儲存資料,也能使用陣列的元素儲存資料。那麼兩者有何差別呢?

使用物件:

classUser
{
public $uid;
public $username;
public $age;
function getUserInfo()
    {
return "UID:".$this->uid." UserName:".$this->username." Age:".$this->age;
    }
}
for($i=0; $i<1000000;$i++) {
$user = new User();
$user->uid= $i;
$user->age = $i%100;
$user->username="User".$i;
$user->getUserInfo();
}

使用陣列:

functiongetUserInfo($user)
{
return "UID:".$user['uid']." UserName:".$user['username']." Age:".$user['age'];
}
for($i=0; $i<1000000;$i++) {
$user = array("uid"=>$i,"age" =>$i%100,"username"=>"User".$i);
getUserInfo($user);
}

我們分別在 PHP5.3、PHP 5.6 和 PHP 7 中執行這兩段程式碼。

[root@localhostphpperf]# time phpobject.php
real 0m2.144s 
user 0m2.119s 
sys 0m0.009s 
[root@localhostphpperf]# time phpobject.php
real 0m2.106s 
user 0m2.089s 
sys 0m0.013s 
[root@localhostphpperf]# time php object1.php
real 0m1.421s 
user 0m1.402s 
sys 0m0.016s 
[root@localhostphpperf]# time php object1.php
real 0m1.431s 
user 0m1.410s 
sys 0m0.012s

在 PHP 5.3 中,陣列版比物件版快了近30%。

[root@localhostphpperf]# time php56 object.php
real 0m1.323s 
user 0m1.319s 
sys 0m0.002s 
[root@localhostphpperf]# time php56 object.php
real 0m1.414s 
user 0m1.400s 
sys 0m0.006s 
[root@localhostphpperf]# time php56 object1.php
real 0m1.356s 
user 0m1.352s 
sys 0m0.002s 
[root@localhostphpperf]# time php56 object1.php
real 0m1.364s 
user 0m1.349s 
sys 0m0.006s 
[root@localhostphpperf]# time php7 object.php
real 0m0.642s 
user 0m0.638s 
sys 0m0.003s 
[root@localhostphpperf]# time php7 object.php
real 0m0.606s 
user 0m0.602s 
sys 0m0.003s 
[root@localhostphpperf]# time php7 object1.php
real 0m0.615s 
user 0m0.613s 
sys 0m0.000s 
[root@localhostphpperf]# time php7 object1.php
real 0m0.615s 
user 0m0.611s 
sys 0m0.003s

到了 PHP 5.6 和 PHP7 中,兩個版本基本沒有差別,而在 PHP7 中的速度是 PHP5.6 中的2倍。在新的版本中,差別已幾乎沒有,那麼為了清楚起見我們當然應該宣告類,例項化類來儲存物件資料。

2.8、getter 和 setter

從 Java 轉過來學習 PHP 的朋友,在物件宣告時,可能習慣使用 getter 和 setter,那麼,在 PHP 中,使用 getter 和 setter 是否會帶來效能上的損失呢?同樣,先上例子。

無 setter版:

classUser
{
public $uid;
public $username;
public $age;
function getUserInfo()
    {
return "UID:".$this->uid." UserName:".$this->username." Age:".$this->age;
    }
}
for($i=0; $i<1000000;$i++) {
$user = new User();
$user->uid= $i;
$user->age = $i%100;
$user->username="User".$i;
$user->getUserInfo();
}

有 setter版:

classUser
{
public $uid;
private $username;
public $age;
function setUserName($name)
    {
$this->username = $name;
    }
function getUserInfo()
    {
return "UID:".$this->uid." UserName:".$this->username." Age:".$this->age;
    }
}
for($i=0; $i<1000000;$i++) {
$user = new User();
$user->uid= $i;
$user->age = $i%100;
$user->setUserName("User".$i);
$user->getUserInfo();
}

這裡只增加了一個 setter。執行結果如下:

[root@localhostphpperf]# time php7 object.php
real 0m0.607s 
user 0m0.602s 
sys 0m0.004s 
[root@localhostphpperf]# time php7 object.php
real 0m0.598s 
user 0m0.596s 
sys 0m0.000s 
[root@localhostphpperf]# time php7 object2.php
real 0m0.673s 
user 0m0.669s 
sys 0m0.003s 
[root@localhostphpperf]# time php7 object2.php
real 0m0.668s 
user 0m0.664s 
sys 0m0.004s

從上面可以看到,增加了一個 setter,帶來了近10%的效率損失。可見這個效能損失是相當大的,在 PHP 中,我們沒有必要再來做 setter 和 getter了。需要引用的屬性,直接使用即可。

2.9、類屬性該宣告還是不宣告

PHP 本身支援屬性可以在使用時增加,也就是不宣告屬性,可以在執行時新增屬性。那麼問題來了,事先宣告屬性與事後增加屬性,是否會有效能上的差別。這裡也舉一個例子探討一下。

事先宣告瞭屬性的程式碼就是2.8節中,無 setter 的程式碼,不再重複。而無屬性宣告的程式碼如下:

classUser
{ 
function getUserInfo()
    {
return "UID:".$this->uid." UserName:".$this->username." Age:".$this->age;
    }
}
for($i=0; $i<1000000;$i++) {
$user = new User();
$user->uid= $i;
$user->age = $i%100;
$user->username="User".$i;
$user->getUserInfo();
}

兩段程式碼,執行結果如下:

[root@localhostphpperf]# time php7 object.php
real 0m0.608s 
user 0m0.604s 
sys 0m0.003s 
[root@localhostphpperf]# time php7 object.php
real 0m0.615s 
user 0m0.605s 
sys 0m0.003s 
[root@localhostphpperf]# time php7 object3.php
real 0m0.733s 
user 0m0.728s 
sys 0m0.004s 
[root@localhostphpperf]# time php7 object3.php
real 0m0.727s 
user 0m0.720s 
sys 0m0.004s

從上面的執行可以看到,無屬性宣告的程式碼慢了20%。可以推斷出來的就是對於物件的屬性,如果事先知道的話,我們還是事先宣告的好,這一方面是效率問題,另一方面,也有助於提高程式碼的可讀性呢。

2.10、圖片操作 API 的效率差別

在圖片處理操作中,一個非常常見的操作是將圖片縮放成小圖。縮放成小圖的辦法有多種,有使用 API 的,有使用命令列的。在 PHP 中,有 iMagick 和 gmagick 兩個擴充套件可供操作,而命令列則一般使用 convert 命令來處理。我們這裡來討論使用 imagick 擴充套件中的 API 處理圖片的效率差別。

先上程式碼:

function imagick_resize($filename, $outname)
{
$thumbnail = new Imagick($filename);
$thumbnail->resizeImage(200, 200, imagick::FILTER_LANCZOS, 1);
$thumbnail->writeImage($outname);
unset($thumbnail);
}
function imagick_scale($filename, $outname)
{
$thumbnail = new Imagick($filename);
$thumbnail->scaleImage(200, 200);
$thumbnail->writeImage($outname);
unset($thumbnail);
}
function convert($func)
{
$cmd= "find /var/data/ppt |grep jpg";
$start = microtime(true);
exec($cmd, $files);
$index = 0;
foreach($files as $key =>$filename)
    {
$outname= " /tmp/$func"."_"."$key.jpg";
$func($filename, $outname);
$index++;
    }
$end = microtime(true);
echo "$func $index files: " . ($end- $start) . "s\n";
}
convert("imagick_resize");
convert("imagick_scale");

在上面的程式碼中,我們分別使用了 resizeImage 和 scaleImage 來進行圖片的壓縮,壓縮的是常見的 1-3M 之間的數位相機圖片,得到如下執行結果:

[root@localhostphpperf]# php55 imagick.php
imagick_ resize 169 files: 5.0612308979034s 
imagick_ scale 169 files: 3.1105840206146s
[root@localhostphpperf]# php55 imagick.php
imagick_ resize 169 files: 4.4953861236572s 
imagick_ scale 169 files: 3.1514940261841s
[root@localhostphpperf]# php55 imagick.php
imagick_ resize 169 files: 4.5400381088257s 
imagick_ scale 169 files: 3.2625908851624s

169張圖片壓縮,使用 resizeImage 壓縮,速度在4.5S以上,而使用 scaleImage 則在 3.2S 左右,快了將近50%,壓縮的效果,用肉眼看不出明顯區別。當然 resizeImage 的控制能力更強,不過對於批量處理而言,使用 scaleImage 是更好的選擇,尤其對頭像壓縮這種頻繁大量的操作。本節只是例舉了圖片壓縮 API 作為例子,也正像 explode 和 preg_ split 一樣,在 PHP 中,完成同樣一件事情,往往有多種手法。建議採用效率高的做法。

以上就是關於 PHP 開發的10個方面的對比,這些點涉及到 PHP 語法、寫法以及 API 的使用。有些策略隨著 PHP 的發展,有的已經不再適用,有些策略則會一直有用。

有童鞋也許會說,在現實的開發應用中,上面的某些觀點和解決策略,有點「然並卵」。為什麼這麼說呢?因為在一個程式的效能瓶頸中,最為核心的瓶頸,往往並不在 PHP 語言本身。即使是跟 PHP 程式碼中暴露出來的效能瓶頸,也常在外部資源和程式的不良寫法導致的瓶頸上。於是為了做好效能分析,我們需要向 PHP 的上下游戲延伸,比如延伸到後端的服務上去,比如延伸到前端的優化規則。在這兩塊,都有了相當多的積累和分析,雅虎也據此提出了多達35條前端優化規則,這些同 PHP 本身的效能分析構成了一個整體,就是降低使用者的訪問延時。

所以前面兩部分所述的效能分析,只是有助於大家瞭解 PHP 開發本身,寫出更好的 PHP 程式,為你成為一個資深的 PHP 程式設計師打下基礎,對於實際生產中程式的效率提升,往往幫助也不是特別顯著,因為大家也看到,在文章的例項中,很多操作往往是百萬次才能看出明顯的效能差別。在現實的頁面中,每一個請求很快執行完成,對這些基礎程式碼的呼叫,往往不會有這麼多次呼叫。不過了解這些,總是好的。

那麼,對於一個程式而言,其他的效能瓶頸可能存在哪裡?我們將深入探討。所以在本系列的下兩篇,我們將探討 PHP 程式的外圍效源的效率問題和前端效率問題,敬請期待。

相關文章