前言
在任何語言中,函數(shù)都是最基本的組成單元。對于php的函數(shù),它具有哪些特點?函數(shù)調(diào)用是怎么實現(xiàn)的?php函數(shù)的性能如何,有什么使用建議?本文 將從原理出發(fā)進行分析結(jié)合實際的性能測試嘗試對這些問題進行回答,在了解實現(xiàn)的同時更好的編寫php程序。同時也會對一些常見的php函數(shù)進行介紹。
php函數(shù)的分類
在php中,橫向劃分的話,函數(shù)分為兩大類: user function(內(nèi)置函數(shù)) 和internal function(內(nèi)置函數(shù))。前者就是用戶在程序中自定義的一些函數(shù)和方法,后者則是php本身提供的各類庫函數(shù)(比如sprintf、 array_push等)。用戶也可以通過擴展的方法來編寫庫函數(shù),這個將在后面介紹。對于user function,又可以細分為function(函數(shù))和method(類方法),本文中將就這三種函數(shù)分別進行分析和測試。
推薦教程:PHP視頻教程
php函數(shù)的實現(xiàn)
一個php函數(shù)最終是如何執(zhí)行,這個流程是怎么樣的呢?
要回答這個問題,我們先來看看php代碼的執(zhí)行所經(jīng)過的流程。
從圖中可以看到,php實現(xiàn)了一個典型的動態(tài)語言執(zhí)行過程:拿到一段代碼后,經(jīng)過詞法解析、語法解析等階段后,源程序會被翻譯成一個個指令 (opcodes),然后ZEND虛擬機順次執(zhí)行這些指令完成操作。Php本身是用c實現(xiàn)的,因此最終調(diào)用的也都是c的函數(shù),實際上,我們可以把php看 做是一個c開發(fā)的軟件。
通過上面描述不難看出,php中函數(shù)的執(zhí)行也是被翻譯成了opcodes來調(diào)用,每次函數(shù)調(diào)用實際上是執(zhí)行了一條或多條指令。
對于每一個函數(shù),zend都通過以下的數(shù)據(jù)結(jié)構(gòu)來描述
typedef union _zend_function { zend_uchar type; /* MUST be the first element of this struct! */ struct { zend_uchar type; /* never used */ char *function_name; zend_class_entry *scope; zend_uint fn_flags; union _zend_function *prototype; zend_uint num_args; zend_uint required_num_args; zend_arg_info *arg_info; zend_bool pass_rest_by_reference; unsigned char return_reference; } common; zend_op_array op_array; zend_internal_function internal_function; } zend_function; typedef struct _zend_function_state { HashTable *function_symbol_table; zend_function *function; void *reserved[ZEND_MAX_RESERVED_RESOURCES]; } zend_function_state;
其中type標明了函數(shù)的類型:用戶函數(shù)、內(nèi)置函數(shù)、重載函數(shù)。Common中包含函數(shù)的基本信息,包括函數(shù)名,參數(shù)信息,函數(shù)標志(普通函數(shù)、靜態(tài)方法、抽象方法)等內(nèi)容。另外,對于用戶函數(shù),還有一個函數(shù)符號表,記錄了內(nèi)部變量等,這個將在后面詳述。 Zend維護了一個全局function_table,這是一個大的hahs表。函數(shù)調(diào)用的時候會首先根據(jù)函數(shù)名從表中找到對應的zend_function。當進行函數(shù)調(diào)用時候,虛擬機會根據(jù)type的不同決定調(diào)用方法, 不同類型的函數(shù),其執(zhí)行原理是不相同的 。
內(nèi)置函數(shù)
內(nèi)置函數(shù),其本質(zhì)上就是真正的c函數(shù),每一個內(nèi)置函數(shù),php在最終編譯后都會展開成為一個名叫zif_xxxx的function,比如我們常見 的sprintf,對應到底層就是zif_sprintf。Zend在執(zhí)行的時候,如果發(fā)現(xiàn)是內(nèi)置函數(shù),則只是簡單的做一個轉(zhuǎn)發(fā)操作。
Zend提供了一系列的api供調(diào)用,包括參數(shù)獲取、數(shù)組操作、內(nèi)存分配等。內(nèi)置函數(shù)的參數(shù)獲取,通過zend_parse_parameters 方法來實現(xiàn),對于數(shù)組、字符串等參數(shù),zend實現(xiàn)的是淺拷貝,因此這個效率是很高的??梢赃@樣說,對于php內(nèi)置函數(shù),其效率和相應c函數(shù)幾乎相同,唯 一多了一次轉(zhuǎn)發(fā)調(diào)用。
內(nèi)置函數(shù)在php中都是通過so的方式進行動態(tài)加載,用戶也可以根據(jù)需要自己編寫相應的so,也就是我們常說的擴展。ZEND提供了一系列的api供擴展使用
用戶函數(shù)
和內(nèi)置函數(shù)相比,用戶通過php實現(xiàn)的自定義函數(shù)具有完全不同的執(zhí)行過程和實現(xiàn)原理。如前文所述,我們知道php代碼是被翻譯成為了一條條 opcode來執(zhí)行的,用戶函數(shù)也不例外,實際中每個函數(shù)對應到一組opcode,這組指令被保存在zend_function中。于是,用戶函數(shù)的調(diào)用 最終就是對應到一組opcodes的執(zhí)行。
局部變量的保存及遞歸的實現(xiàn)
我們知道,函數(shù)遞歸是通過堆棧來完成的。在php中,也是利用類似的方法來實現(xiàn)。Zend為每個php函數(shù) 分配了一個活動符號表(active_sym_table),記錄當前函數(shù)中所有局部變量的狀態(tài)。所有的符號表通過堆棧的形式來維護,每當有函數(shù)調(diào)用的時 候,分配一個新的符號表并入棧。當調(diào)用結(jié)束后當前符號表出棧。由此實現(xiàn)了狀態(tài)的保存和遞歸。
對于棧的維護,zend在這里做了優(yōu)化。預先分配一個長度為N的靜態(tài)數(shù)組來模擬堆棧,這種通過靜態(tài)數(shù)組來模擬動態(tài)數(shù)據(jù)結(jié)構(gòu)的手法在我們自己的程序中 也經(jīng)常有使用,這種方式避免了每次調(diào)用帶來的內(nèi)存分配、銷毀。ZEND只是在函數(shù)調(diào)用結(jié)束時將當前棧頂?shù)姆柋頂?shù)據(jù)clean掉即可。
因為靜態(tài)數(shù) 組長度為N,一旦函數(shù)調(diào)用層次超過N,程序不會出現(xiàn)棧溢出,這種情況下zend就會進行符號表的分配、銷毀,因此會導致性能下降很多。在zend里面,N 目前取值是32。因此,我們編寫php程序的時候,函數(shù)調(diào)用層次最好不要超過32。當然,如果是web應用,本身可以函數(shù)調(diào)用層次的深度。
參數(shù)的傳遞
和內(nèi)置函數(shù)調(diào)用zend_parse_params來獲取參數(shù)不同,用戶函數(shù)中參數(shù)的獲取是通過指令來完成的。函數(shù)有幾個參數(shù)就對應幾條指令。具體到實現(xiàn)上就是普通的變量賦值。
通過上面的分析可以看出,和內(nèi)置函數(shù)相比,由于是自己維護堆棧表,而且每條指令的執(zhí)行也是一個c函數(shù),用戶函數(shù)的性能相對會差很多,后面會有具體的對比分析。因此,如果一個功能有對應php內(nèi)置函數(shù)實現(xiàn)的盡量不要自己重新寫函數(shù)去實現(xiàn)。
類方法
類方法其執(zhí)行原理和用戶函數(shù)是相同的,也是翻譯成opcodes順次調(diào)用。類的實現(xiàn),zend用一個數(shù)據(jù)結(jié)構(gòu)zend_class_entry來實現(xiàn),里面保存了類相關的一些基本信息。這個entry是在php編譯的時候就已經(jīng)處理完成。
在zend_function的common中,有一個成員叫做scope,其指向的就是當前方法對應類的zend_class_entry。關于 php中面向?qū)ο蟮膶崿F(xiàn),這里就不在做更詳細的介紹,今后將專門寫一篇文章來詳述php中面向?qū)ο蟮膶崿F(xiàn)原理。就函數(shù)這一塊來說,method實現(xiàn)原理和 function完全相同,理論上其性能也差不多,后面我們將做詳細的性能對比。
函數(shù)名長度對性能的影響
測試方法
對名字長度為1、2、4、8、16的函數(shù)進行比較,測試比較它們每秒可執(zhí)行次數(shù),確定函數(shù)名長度對性能的影響
測試結(jié)果如下圖
結(jié)果分析
從圖上可以看出,函數(shù)名的長度對性能還是會有一定的影響。一個長度為1的函數(shù)和長度為16的 空函數(shù)調(diào)用 ,其性能差了1倍。分析一下源碼不難找到原因,如前面敘述所說,函數(shù)調(diào)用的時候zend會先在一個全局的funtion_table中通過函數(shù)名查詢相關信息,function_table是一個哈希表。必然的,名字越長查詢所需要的時間就越多。 因此,在實際編寫程序的時候,對多次調(diào)用的函數(shù),名字建議不要太長
雖然函數(shù)名長度對性能有一定影響,但具體有多大呢?這個問題應該還是需要結(jié)合實際情況來考慮,如果一個函數(shù)本身比較復雜的話,那么對整體的性能影響并不大。
一個建議是對于那些會調(diào)用很多次,本身功能又比較簡單的函數(shù),可以適當取一些言簡意賅的名字。
函數(shù)個數(shù)對性能的影響
測試方法
在以下三種環(huán)境下進行函數(shù)調(diào)用測試,分析結(jié)果:1.程序僅包含1個函數(shù) 2.程序包含100個函數(shù) 3.程序包含1000個函數(shù)。
測試這三種情況下每秒所能調(diào)用的函數(shù)次數(shù)
測試結(jié)果如下圖
結(jié)果分析
從測試結(jié)果可以看出,這三種情況下性能幾乎相同,函數(shù)個數(shù)增加時性能下降微乎其微,可以忽略。
從實現(xiàn)原理分析,幾種實現(xiàn)下唯一的區(qū)別在于函數(shù)獲取的部分。如前文所述,所有的函數(shù)都放在一個hash表中,在不同個數(shù)下查找效率都應該還是接近于O(1),所以性能差距不大。
不同類型函數(shù)調(diào)用消耗
測試方法
選取用戶函數(shù)、類方法、靜態(tài)方法、內(nèi)置函數(shù)各一種,函數(shù)本身不做任何事情,直接返回,主要測試空函數(shù)調(diào)用的消耗。測試結(jié)果為每秒可執(zhí)行次數(shù)
測試中為去除其他影響,所有函數(shù)名字長度相同
測試結(jié)果如下圖
結(jié)果分析
通過測試結(jié)果可以看到,對于用戶自己編寫的php函數(shù),不管是哪種類型,其效率是差不多的,均在280w/s左右。如我們預 期,即使是空調(diào),內(nèi)置函數(shù)其效率也要高很多,達到780w/s,是前者是3倍??梢?,內(nèi)置函數(shù)調(diào)用的開銷還是遠低于用戶函數(shù)。從前面原理分析可知主要差距 在于用戶函數(shù)調(diào)用時初始化符號表、接收參數(shù)等操作。
內(nèi)置函數(shù)和用戶函數(shù)性能對比
測試方法
內(nèi)置函數(shù)和用戶函數(shù)的性能對比,這里我們選取幾個常用的函數(shù),然后用php實現(xiàn)相同功能的函數(shù)進行一下性能對比。
測試中,我們選取字符串、數(shù)學、數(shù)組中各一個典型進行對比,這幾個函數(shù)分別是字符串截取(substr)、10進制轉(zhuǎn)2進制(decbin)、求最小值(min)和返回數(shù)組中的所以key(array_keys)。
測試結(jié)果如下圖
結(jié)果分析
從測試結(jié)果可以看出,如我們預期,內(nèi)置函數(shù)在總體性能上遠高于普通用戶函數(shù)。尤其對于涉及到字符串類操作的函數(shù),差距達到了1個數(shù)量級。因此,函數(shù)使用的一個原則就是如果某功能有相應的內(nèi)置函數(shù),盡量使用它而不是自己編寫php函數(shù)。
對于一些涉及到大量字符串操作的功能,為提高性能,可以考慮用擴展來實現(xiàn)。比如常見的富文本過濾等。
和C函數(shù)性能對比
測試方法
我們選取字符串操作和算術運算各3種函數(shù)進行比對,php用擴展實現(xiàn)。三種函數(shù)是簡單的一次算法運算、字符串比較和多次的算法運算。
除了本身的兩類函數(shù)外,還會測試將函數(shù)空調(diào)開銷去掉后的性能,一方面比對一下兩種函數(shù)(c和php內(nèi)置)本身的性能差異,另外就是側(cè)面印證空調(diào)函數(shù)的消耗
測試點為執(zhí)行10w次操作的時間消耗
測試結(jié)果如下圖
結(jié)果分析
內(nèi)置函數(shù)和C函數(shù)的開銷在去掉php函數(shù)空調(diào)用的影響后差距較小,隨著函數(shù)功能越來越復雜,雙方性能趨近于相同。這個從之前的函數(shù)實現(xiàn)分析中也容易得到論證,畢竟內(nèi)置函數(shù)就是C實現(xiàn)的。
函數(shù)功能越復雜,c和php的性能差距越小
相對c來說,php函數(shù)調(diào)用的開銷大很多,對于簡單函數(shù)來說性能還是有一定影響。因此php中函數(shù)不宜嵌套封裝太深。
偽函數(shù)及其性能
在php中,有這樣一些函數(shù),它們在使用上是標準的函數(shù)用法,但底層實現(xiàn)卻和真正函數(shù)調(diào)用完全不同,這些函數(shù)不屬于前文提到的三種function中的任何一類,其實質(zhì)是一條單獨的opcode,這里估且叫做偽函數(shù)或者指令函數(shù)。
如上所說,偽函數(shù)使用起來和標準的函數(shù)并無二致,看起來具有相同的特征。但是他們最終執(zhí)行的時候是被zend反映成了一條對應的指令(opcode)來調(diào)用,因此其實現(xiàn)更接近于if、for、算術運算等操作。
php中的偽函數(shù)
?。薄sset
?。?、empty
?。场nset
?。础val
通過上面的介紹可以看出,偽函數(shù)由于被直接翻譯成指令來執(zhí)行,和普通函數(shù)相比少了一次函數(shù)調(diào)用所帶來的開銷,因此性能會更好一些。我們通過如下測試來做一個對比。 Array_key_exists和isset兩者都可以判斷數(shù)組中某個key是否存在,看一下他們的性能
從圖上可以看出,和array_key_exists相比,isset性能要高出很多,基本是前者的4倍左右,而即使是和空函數(shù)調(diào)用相比,其性能也要高出1倍左右。由此也側(cè)面印證再次說明了php函數(shù)調(diào)用的開銷還是比較大的。