php xingnengfenxi_PHP 性能分析与实验:性能的微观分析
在上一篇文章中,我們從 PHP 是解釋性語言、動態(tài)語言和底層實現(xiàn)等三個方面,探討了 PHP 性能的問題。本文就深入到 PHP 的微觀層面,我們來了解 PHP 在使用和編寫代碼過程中,性能方面,可能需要注意和提升的地方。
在開始分析之前,我們得掌握一些與性能分析相關的函數(shù)。這些函數(shù)讓我們對程序性能有更好的分析和評測。
一、性能分析相關的函數(shù)與命令
1.1、時間度量函數(shù)
平時我們常用 time() 函數(shù),但是返回的是秒數(shù),對于某段代碼的內部性能分析,到秒的精度是不夠的。于是要用 microtime 函數(shù)。而 microtime 函數(shù)可以返回兩種形式,一是字符串的形式,一是浮點數(shù)的形式。不過需要注意的是,在缺省的情況下,返回的精度只有4位小數(shù)。為了獲得更高的精確度,我們需要配置 precision。
如下是 microtime 的使用結果。$start=?microtime(true);echo?$start."/n";
$end?=?microtime(true);echo?$end."/n";echo?($end-$start)."/n";
輸出為:bash-3.2#?phptime.php1441360050.3286
1441360050.3292
0.00053000450134277
而在代碼前面加上一行:ini_set("precision",?16);
輸出為:bash-3.2#?phptime.php1441360210.932628
1441360210.932831
0.0002031326293945312
除了 microtime 內部統(tǒng)計之外, 還可以使用 getrusage 來取得用戶態(tài)的時長。在實際的操作中,也常用 time 命令來計算整個程序的運行時長,通過多次運行或者修改代碼后運行,得到不同的時間長度以得到效率上的區(qū)別。 具體用法是:time phptime.php ,則在程序運行完成之后,不管是否正常結束退出,都會有相關的統(tǒng)計。bash-3.2#?time?phptime.php1441360373.150756
1441360373.150959
0.0002031326293945312real?0m0.186s?user?0m0.072s?sys?0m0.077s
因為本文所討論的性能問題,往往分析上百萬次調用之后的差距與趨勢,為了避免代碼中存在一些時間統(tǒng)計代碼,后面我們使用 time 命令居多。
1.2、內存使用相關函數(shù)
分析內存使用的函數(shù)有兩個:memory_ get_ usage、memory_ get_ peak_usage,前者可以獲得程序在調用的時間點,即當前所使用的內存,后者可以獲得到目前為止高峰時期所使用的內存。所使用的內存以字節(jié)為單位。$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.phpHello,world!
224400?224568?168?227424
可以看到,即使程序中間只輸出了一句話,再加上變量存儲,也消耗了168個字節(jié)的內存。
對于同一程序,不同 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.php0:?93784?bytes?10000:?93784?bytes
……?80000:?93784?bytes?90000:?93784?bytes
peak:?262144?bytes
PHP 5.3 中,內存使用如下[root@localhostphpperf]#?phpmemory.php0:?634992?bytes?10000:?634992?bytes
……?80000:?634992?bytes?90000:?634992?bytes
peak:?786432?bytes
可見 PHP 5.3 在內存使用上要粗放了一些。
PHP 5.4 – 5.6 差不多,有所優(yōu)化:[root@localhostphpperf]#?php56?memory.php0:?224944?bytes?10000:?224920?bytes
……?80000:?224920?bytes?90000:?224920?bytes
peak:?262144?bytes
而 PHP 7 在少量使用時,高峰內存的使用,增大很多。[root@localhostphpperf]#?php7?memory.php0:?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";
這時候再來看看內存的使用情況,中間表格主體部分為內存使用量,單位為字節(jié)。
圖表如下:
PHP 5.2 并沒有合適的垃圾回收機制,導致內存使用越來越多。而5.3 以后內存回收機制導致內存穩(wěn)定在一個區(qū)間。而也可以看見 PHP7 內存使用最少。把 PHP 5.2 的圖形去掉了之后,對比更為明顯。
可見 PHP7 不僅是在算法效率上,有大幅度的提升,在大批量內存使用上也有大幅度的優(yōu)化(盡管小程序的高峰內存比歷史版本所用內存更多)。
1.3、垃圾回收相關函數(shù)
在 PHP 中,內存回收是可以控制的,我們可以顯式地關閉或者打開垃圾回收,一種方法是通過修改配置,zend.enable_gc=Off?就可以關掉垃圾回收。缺省情況下是?On?的。另外一種手段是通過?gc _enable()和gc _disable()函數(shù)分別打開和關閉垃圾回收。
比如在上面的例子的基礎上,我們關閉垃圾回收,就可以得到如下數(shù)據(jù)表格和圖表。
代碼如下: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 下運行,得到如下內存使用統(tǒng)計表。
圖表如下,PHP7 還是內存使用效率最優(yōu)的。
從上面的例子也可以看出來,盡管在第一個例子中,PHP7 的高峰內存使用數(shù)是最多的,但是當內存使用得多時,PHP7 的內存優(yōu)化就體現(xiàn)出來了。
這里值得一提的是垃圾回收,盡管會使內存減少,但是會導致速度降低,因為垃圾回收也是需要消耗 CPU 等其他系統(tǒng)資源的。Composer 項目就曾經(jīng)因為在計算依賴前關閉垃圾回收,帶來成倍性能提升,引發(fā)廣大網(wǎng)友關注。詳見:
在常見的代碼和性能分析中,出了以上三類函數(shù)之外,還常使用的有堆棧跟蹤函數(shù)、輸出函數(shù),這里不再贅述。
二、PHP 性能分析10則
下面我們根據(jù)小程序來驗證一些常見的性能差別。
2.1、使用 echo 還是 print
在有的建議規(guī)則中,會建議使用 echo ,而不使用 print。說 print 是函數(shù),而 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/nullreal?0m0.233s
user?0m0.153s
sys?0m0.080s
[root@localhostphpperf]#?time?php?echo1.php?>?/dev/nullreal?0m0.234s
user?0m0.159s
sys?0m0.073s
[root@localhostphpperf]#?time?phpecho.php>?/dev/nullreal?0m0.203s
user?0m0.130s
sys?0m0.072s
[root@localhostphpperf]#?time?phpecho.php>?/dev/nullreal?0m0.203s
user?0m0.128s
sys?0m0.075s
在 PHP5.3 版中效率差距10%以上。而在 PHP5.4 以上的版本中,區(qū)別不大,如下是 PHP7 中的運行效率。[root@localhostphpperf]#?time?php7?echo.php>?/dev/nullreal?0m0.151s
user?0m0.088s
sys?0m0.062s
[root@localhostphpperf]#?time?php7?echo.php>?/dev/nullreal?0m0.145s
user?0m0.084s
sys?0m0.061s
[root@localhostphpperf]#?time?php7?echo1.php?>?/dev/nullreal?0m0.140s
user?0m0.075s
sys?0m0.064s
[root@localhostphpperf]#?time?php7?echo1.php?>?/dev/nullreal?0m0.146s
user?0m0.077s
sys?0m0.069s
正如瀏覽器前端的一些優(yōu)化準則一樣,沒有啥特別通用的原則,往往根據(jù)不同的情況和版本,規(guī)則也會存在不同。
2.2、require 還是 require_once?
在一些常規(guī)的優(yōu)化規(guī)則中,會提到,建議使用 require_ once 而不是 require,現(xiàn)由是 require_ once 會去檢測是否重復,而 require 則不需要重復檢測。
在大量不同文件的包含中,require_ once 略慢于 require。但是 require_ once 的檢測是一項內存中的行為,也就是說即使有數(shù)個需要加載的文件,檢測也只是內存中的比較。而 require 的每次重新加載,都會從文件系統(tǒng)中去讀取分析。因而 require_ once 會比 require 更佳。咱們也使用一個例子來看一下。str.phpglobal$str;
$str=?"China?has?a?large?population";require.phpfor($i=0;?$i<100000;?$i++)?{require?"str.php";
}require_once.phpfor($i=0;?$i<100000;?$i++)?{require_once"str.php";
}
上面的例子,在 PHP7 中,require_ once.php 的運行速度是 require.php 的30倍!在其他版本也能得到大致相同的結果。[root@localhostphpperf]#?time?php7?require.phpreal?0m1.712s?user?0m1.126s?sys?0m0.569s?[root@localhostphpperf]#?time?php7?require.phpreal?0m1.640s?user?0m1.113s?sys?0m0.515s?[root@localhostphpperf]#?time?php7?require_once.phpreal?0m0.066s?user?0m0.063s?sys?0m0.003s?[root@localhostphpperf]#?time?php7?require_once.phpreal?0m0.057s?user?0m0.052s?sys?0m0.004s
從上可以看到,如果存在大量的重復加載的話,require_ once 明顯優(yōu)于 require,因為重復的文件不再有 IO 操作。即使不是大量重復的加載,也建議使用 require_ once,因為在一個程序中,一般不會存在數(shù)以千百計的文件包含,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 類中,有四個不同的方法,完成一樣的功能,就是拼接信息返回,看看這四個不同的方法的區(qū)別。
第一個、getUserInfo?,使用雙引號和屬性相拼接[root@localhostphpperf]#?time?php7?string.phpreal?0m0.670s?user?0m0.665s?sys?0m0.002s?[root@localhostphpperf]#?time?php7?string.phpreal?0m0.692s?user?0m0.689s?sys?0m0.002s?[root@localhostphpperf]#?time?php7?string.phpreal?0m0.683s?user?0m0.672s?sys?0m0.004s
第二個、getUserInfoSingle?,使用單引號和屬性相拼接[root@localhostphpperf]#?time?php7?string.phpreal?0m0.686s?user?0m0.683s?sys?0m0.001s?[root@localhostphpperf]#?time?php7?string.phpreal?0m0.671s?user?0m0.666s?sys?0m0.003s?[root@localhostphpperf]#?time?php7?string.phpreal?0m0.669s?user?0m0.666s?sys?0m0.002s
可見在拼接中,單雙引號并無明顯差別。
第三個、getUserInfoOnce,不再使用句號.連接,而是直接引入在字符串中解析。[root@localhostphpperf]#?time?php7?string.phpreal?0m0.564s?user?0m0.556s?sys?0m0.006s?[root@localhostphpperf]#?time?php7?string.phpreal?0m0.592s?user?0m0.587s?sys?0m0.004s?[root@localhostphpperf]#?time?php7?string.phpreal?0m0.563s?user?0m0.559s?sys?0m0.003s
從上面可見,速度提高了0.06s-0.10s,有10%-20%的效率提升。可見連綴效率更低一些。
第四個、getUserInfoSingle2?雖然沒有達到我們真正想要的效果,功能是不正確的,但是在字符串中,不再需要解析變量和獲取變量值,所以效率確實有大幅度提升。[root@localhostphpperf]#?time?php7?string.phpreal?0m0.379s?user?0m0.375s?sys?0m0.003s?[root@localhostphpperf]#?time?php7?string.phpreal?0m0.399s?user?0m0.394s?sys?0m0.003s?[root@localhostphpperf]#?time?php7?string.phpreal?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.phpreal?0m0.385s?user?0m0.381s?sys?0m0.002s?[root@localhostphpperf]#?time?php7?string.phpreal?0m0.382s?user?0m0.380s?sys?0m0.002s?[root@localhostphpperf]#?time?php7?string.phpreal?0m0.386s?user?0m0.380s?sys?0m0.004s
發(fā)現(xiàn)轉義后的字符串,效率跟單引號是一致的,從這里也可以看見,單引號還是雙引號包含,如果不存在需要解析的變量,幾乎沒有差別。如果有需要解析的變量,你也不能光用單引號,要么使用單引號和連綴,要么使用內部插值,所以在這條規(guī)則上,不用太過糾結。
2.4、錯誤應該打開還是關閉?
在 PHP 中,有多種錯誤消息,錯誤消息的開啟是否會帶來性能上的影響呢?從直覺覺得,由于錯誤消息,本身會涉及到 IO 輸出,無論是輸出到終端或者 error_log,都是如此,所以肯定會影響性能。我們來看看這個影響有多大。error_reporting(E_ERROR);for($i=0;?$i<1000000;$i++)?{
$str=?"通常,$PHP中的垃圾回收機制,僅僅在循環(huán)回收算法確實運行時會有時間消耗上的增加。但是在平常的(更小的)腳本中應根本就沒有性能影響。
然而,在平常腳本中有循環(huán)回收機制運行的情況下,內存的節(jié)省將允許更多這種腳本同時運行在你的服務器上。因為總共使用的內存沒達到上限。";
}
在上面的代碼中,我們涉及到一個不存在的變量,所以會報出 Notice 錯誤:Notice:?Undefined?variable:?PHP?中的垃圾回收機制,僅僅在循環(huán)回收算法確實運行時會有時間消耗上的增加。但是在平常的?in?xxxx/string2.php?on?line?10
如果把 E_ ERROR 改成 E_ ALL 就能看到大量的上述錯誤輸出。
我們先執(zhí)行 E_ ERROR 版,這個時候沒有任何錯誤日志輸出。得到如下數(shù)據(jù):[root@localhostphpperf]#?time?php7?string2.phpreal?0m0.442s?user?0m0.434s?sys?0m0.005s?[root@localhostphpperf]#?time?php7?string2.phpreal?0m0.487s?user?0m0.484s?sys?0m0.002s?[root@localhostphpperf]#?time?php7?string2.phpreal?0m0.476s?user?0m0.471s?sys?0m0.003s
再執(zhí)行 E_ ALL 版,有大量的錯誤日志輸出,我們把輸出重定向到/dev/null[root@localhostphpperf]#?time?php7?string2.php?>?/dev/nullreal?0m0.928s
user?0m0.873s
sys?0m0.051s
[root@localhostphpperf]#?time?php7?string2.php?>?/dev/nullreal?0m0.984s
user?0m0.917s
sys?0m0.064s
[root@localhostphpperf]#?time?php7?string2.php?>?/dev/nullreal?0m0.945s
user?0m0.887s
sys?0m0.056s
可見慢了將近一倍。
如上可見,即使輸出沒有正式寫入文件,錯誤級別打開的影響也是巨大的。在線上我們應該將錯誤級別調到 E_ ERROR 這個級別,同時將錯誤寫入 error_ log,既減少了不必要的錯誤信息輸出,又避免泄漏路徑等信息,造成安全隱患。
2.5、正則表達式和普通字符串操作
在字符串操作中,有一條常見的規(guī)則,即是能使用普通字符串操作方法替代的,就不要使用正則表達式來處理,用 C 語言操作 PCRE 做過正則表達式處理的童鞋應該清楚,需要先 compile,再 exec,也就是說是一個相對復雜的過程。現(xiàn)在就比較一下兩者的差別。
對于簡單的分隔,我們可以使用 explode 來實現(xiàn),也可以使用正則表達式,比如下面的例子: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.phpreal?0m0.941s?user?0m0.931s?sys?0m0.007s?[root@localhostphpperf]#?time?php7?pregstring.phpreal?0m0.986s?user?0m0.980s?sys?0m0.004s?[root@localhostphpperf]#?time?php7?pregstring.phpreal?0m1.004s?user?0m0.998s?sys?0m0.003s
我們再將分隔語句替換成:list($usec,?$sec)?=?preg_split("#\s#",?microtime());
得到如下數(shù)據(jù),慢了近10-20%。[root@localhostphpperf]#?time?php7?pregstring1.phpreal?0m1.195s?user?0m1.182s?sys?0m0.004s?[root@localhostphpperf]#?time?php7?pregstring1.phpreal?0m1.222s?user?0m1.217s?sys?0m0.003s?[root@localhostphpperf]#?time?php7?pregstring1.phpreal?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.phpreal?0m0.172s?user?0m0.167s?sys?0m0.003s?[root@localhostphpperf]#?time?php7?pregstring2.phpreal?0m0.199s?user?0m0.196s?sys?0m0.002s?[root@localhostphpperf]#?time?php7?pregstring3.phpreal?0m0.185s?user?0m0.182s?sys?0m0.003s?[root@localhostphpperf]#?time?php7?pregstring3.phpreal?0m0.184s?user?0m0.181s?sys?0m0.003s
兩者區(qū)別不大。再看看在 PHP5.6 中的表現(xiàn)。[root@localhostphpperf]#?time?php56?pregstring2.phpreal?0m0.470s?user?0m0.456s?sys?0m0.004s?[root@localhostphpperf]#?time?php56?pregstring2.phpreal?0m0.506s?user?0m0.500s?sys?0m0.005s?[root@localhostphpperf]#?time?php56?pregstring3.phpreal?0m0.348s?user?0m0.342s?sys?0m0.004s?[root@localhostphpperf]#?time?php56?pregstring3.phpreal?0m0.376s?user?0m0.364s?sys?0m0.003s
可見在 PHP 5.6 中表現(xiàn)還是非常明顯的,使用正則表達式慢了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)
{
}
}
這是一個動態(tài)編譯的 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.phpreal?0m0.351s?user?0m0.346s?sys?0m0.004s?[root@localhostphpperf]#?time?php7?pregstring2.phpreal?0m0.359s?user?0m0.352s?sys?0m0.004s?[root@localhostphpperf]#?time?php7?pregstring3.phpreal?0m0.375s?user?0m0.369s?sys?0m0.003s?[root@localhostphpperf]#?time?php7?pregstring3.phpreal?0m0.370s?user?0m0.365s?sys?0m0.005s
可見兩者并不明顯。而在 PHP 5.6 中,同樣的代碼:[root@localhostphpperf]#?time?php56?pregstring2.phpreal?0m1.022s?user?0m1.015s?sys?0m0.005s?[root@localhostphpperf]#?time?php56?pregstring2.phpreal?0m1.049s?user?0m1.041s?sys?0m0.005s?[root@localhostphpperf]#?time?php56?pregstring3.phpreal?0m0.923s?user?0m0.821s?sys?0m0.002s?[root@localhostphpperf]#?time?php56?pregstring3.phpreal?0m0.838s?user?0m0.831s?sys?0m0.004s
在 PHP 5.6 中,stripos 版明顯要快于正則表達式版,由上兩例可見,PHP7對正則表達式的優(yōu)化還是相當驚人的。其次也建議,能用普通字符串操作的地方,可以避免使用正則表達式。因為在其他版本中,這個規(guī)則還是適用的。某 zend 大牛官方的分享給出如下數(shù)據(jù):stripos(‘http://’, $website)?速度是preg_match(‘/http:\/\//i’, $website)?的兩倍
ctype_alnum()速度是preg_match(‘/^\s*$/’)的5倍;
“if ($test == (int)$test)”?比?preg_match(‘/^\d*$/’)快5倍
可以相見,正則表達式是相對低效的。
2.6、數(shù)組元素定位查找
在數(shù)組元素的查找中,有一個關鍵的注意點就是數(shù)組值和鍵的查找速度,差異非常大。了解過 PHP 擴展開發(fā)的朋友,應該清楚,數(shù)組在底層其實是 Hash 表。所以鍵是以快速定位的,而值卻未必。下面來看例子。
首先們構造一個數(shù)組:$a=?array();for($i=0;$i<100000;$i++){
$a[$i]?=?$i;
}
在這個數(shù)組中,我們測試查找值和查找鍵的效率差別。
第一種方法用 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.phpreal?0m9.026s?user?0m8.965s?sys?0m0.007s?[root@localhostphpperf]#?time?php7?array.phpreal?0m9.063s?user?0m8.965s?sys?0m0.005s?[root@localhostphpperf]#?time?php7?array1.phpreal?0m0.018s?user?0m0.016s?sys?0m0.001s?[root@localhostphpperf]#?time?php7?array1.phpreal?0m0.021s?user?0m0.015s?sys?0m0.004s?[root@localhostphpperf]#?time?php7?array2.phpreal?0m0.020s?user?0m0.014s?sys?0m0.006s?[root@localhostphpperf]#?time?php7?array2.phpreal?0m0.016s?user?0m0.009s?sys?0m0.006s
由上例子可見,鍵值查找的速度比值查找的速度有百倍以上的效率差別。因而如果能用鍵值定位的地方,盡量用鍵值定位,而不是值查找。
2.7、對象與數(shù)組
在 PHP 中,數(shù)組就是字典,字典可以存儲屬性和屬性值,而且無論是鍵還是值,都不要求數(shù)據(jù)類型統(tǒng)一,所以對象數(shù)據(jù)存儲,既能用對象數(shù)據(jù)結構的屬性存儲數(shù)據(jù),也能使用數(shù)組的元素存儲數(shù)據(jù)。那么兩者有何差別呢?
使用對象: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();
}
使用數(shù)組: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.phpreal?0m2.144s?user?0m2.119s?sys?0m0.009s?[root@localhostphpperf]#?time?phpobject.phpreal?0m2.106s?user?0m2.089s?sys?0m0.013s?[root@localhostphpperf]#?time?php?object1.phpreal?0m1.421s?user?0m1.402s?sys?0m0.016s?[root@localhostphpperf]#?time?php?object1.phpreal?0m1.431s?user?0m1.410s?sys?0m0.012s
在 PHP 5.3 中,數(shù)組版比對象版快了近30%。[root@localhostphpperf]#?time?php56?object.phpreal?0m1.323s?user?0m1.319s?sys?0m0.002s?[root@localhostphpperf]#?time?php56?object.phpreal?0m1.414s?user?0m1.400s?sys?0m0.006s?[root@localhostphpperf]#?time?php56?object1.phpreal?0m1.356s?user?0m1.352s?sys?0m0.002s?[root@localhostphpperf]#?time?php56?object1.phpreal?0m1.364s?user?0m1.349s?sys?0m0.006s?[root@localhostphpperf]#?time?php7?object.phpreal?0m0.642s?user?0m0.638s?sys?0m0.003s?[root@localhostphpperf]#?time?php7?object.phpreal?0m0.606s?user?0m0.602s?sys?0m0.003s?[root@localhostphpperf]#?time?php7?object1.phpreal?0m0.615s?user?0m0.613s?sys?0m0.000s?[root@localhostphpperf]#?time?php7?object1.phpreal?0m0.615s?user?0m0.611s?sys?0m0.003s
到了 PHP 5.6 和 PHP7 中,兩個版本基本沒有差別,而在 PHP7 中的速度是 PHP5.6 中的2倍。在新的版本中,差別已幾乎沒有,那么為了清楚起見我們當然應該聲明類,實例化類來存儲對象數(shù)據(jù)。
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.phpreal?0m0.607s?user?0m0.602s?sys?0m0.004s?[root@localhostphpperf]#?time?php7?object.phpreal?0m0.598s?user?0m0.596s?sys?0m0.000s?[root@localhostphpperf]#?time?php7?object2.phpreal?0m0.673s?user?0m0.669s?sys?0m0.003s?[root@localhostphpperf]#?time?php7?object2.phpreal?0m0.668s?user?0m0.664s?sys?0m0.004s
從上面可以看到,增加了一個 setter,帶來了近10%的效率損失。可見這個性能損失是相當大的,在 PHP 中,我們沒有必要再來做 setter 和 getter了。需要引用的屬性,直接使用即可。
2.9、類屬性該聲明還是不聲明
PHP 本身支持屬性可以在使用時增加,也就是不聲明屬性,可以在運行時添加屬性。那么問題來了,事先聲明屬性與事后增加屬性,是否會有性能上的差別。這里也舉一個例子探討一下。
事先聲明了屬性的代碼就是2.8節(jié)中,無 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.phpreal?0m0.608s?user?0m0.604s?sys?0m0.003s?[root@localhostphpperf]#?time?php7?object.phpreal?0m0.615s?user?0m0.605s?sys?0m0.003s?[root@localhostphpperf]#?time?php7?object3.phpreal?0m0.733s?user?0m0.728s?sys?0m0.004s?[root@localhostphpperf]#?time?php7?object3.phpreal?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 之間的數(shù)碼相機圖片,得到如下運行結果:[root@localhostphpperf]#?php55?imagick.phpimagick_?resize?169?files:?5.0612308979034s?imagick_?scale?169?files:?3.1105840206146s[root@localhostphpperf]#?php55?imagick.phpimagick_?resize?169?files:?4.4953861236572s?imagick_?scale?169?files:?3.1514940261841s[root@localhostphpperf]#?php55?imagick.phpimagick_?resize?169?files:?4.5400381088257s?imagick_?scale?169?files:?3.2625908851624s
169張圖片壓縮,使用 resizeImage 壓縮,速度在4.5S以上,而使用 scaleImage 則在 3.2S 左右,快了將近50%,壓縮的效果,用肉眼看不出明顯區(qū)別。當然 resizeImage 的控制能力更強,不過對于批量處理而言,使用 scaleImage 是更好的選擇,尤其對頭像壓縮這種頻繁大量的操作。本節(jié)只是例舉了圖片壓縮 API 作為例子,也正像 explode 和 preg_ split 一樣,在 PHP 中,完成同樣一件事情,往往有多種手法。建議采用效率高的做法。
以上就是關于 PHP 開發(fā)的10個方面的對比,這些點涉及到 PHP 語法、寫法以及 API 的使用。有些策略隨著 PHP 的發(fā)展,有的已經(jīng)不再適用,有些策略則會一直有用。
有童鞋也許會說,在現(xiàn)實的開發(fā)應用中,上面的某些觀點和解決策略,有點「然并卵」。為什么這么說呢?因為在一個程序的性能瓶頸中,最為核心的瓶頸,往往并不在 PHP 語言本身。即使是跟 PHP 代碼中暴露出來的性能瓶頸,也常在外部資源和程序的不良寫法導致的瓶頸上。于是為了做好性能分析,我們需要向 PHP 的上下游戲延伸,比如延伸到后端的服務上去,比如延伸到前端的優(yōu)化規(guī)則。在這兩塊,都有了相當多的積累和分析,雅虎也據(jù)此提出了多達35條前端優(yōu)化規(guī)則,這些同 PHP 本身的性能分析構成了一個整體,就是降低用戶的訪問延時。
所以前面兩部分所述的性能分析,只是有助于大家了解 PHP 開發(fā)本身,寫出更好的 PHP 程序,為你成為一個資深的 PHP?程序員打下基礎,對于實際生產中程序的效率提升,往往幫助也不是特別顯著,因為大家也看到,在文章的實例中,很多操作往往是百萬次才能看出明顯的性能差別。在現(xiàn)實的頁面中,每一個請求很快執(zhí)行完成,對這些基礎代碼的調用,往往不會有這么多次調用。不過了解這些,總是好的。
那么,對于一個程序而言,其他的性能瓶頸可能存在哪里?我們將深入探討。所以在本系列的下兩篇,我們將探討 PHP 程序的外圍效源的效率問題和前端效率問題,敬請期待。
《新程序員》:云原生和全面數(shù)字化實踐50位技術專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的php xingnengfenxi_PHP 性能分析与实验:性能的微观分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 英伟达驱动更新记录_N卡驱动更新软件(N
- 下一篇: ios删除分割线_iOS应用开发中UIT