轉(zhuǎn)載出處:http://www.cnblogs.com/ghj1976/archive/2010/08/27/1809766.html

前言

在任何語(yǔ)言中,函數(shù)都是最基本的組成單元。對(duì)于php的函數(shù),它具有哪些特點(diǎn)?函數(shù)調(diào)用是怎么實(shí)現(xiàn)的?php函數(shù)的性能如何,有什么使用建議?
本文將從原理出發(fā)進(jìn)行分析結(jié)合實(shí)際的性能測(cè)試嘗試對(duì)這些問(wèn)題進(jìn)行回答,在了解實(shí)現(xiàn)的同時(shí)更好的編寫php程序。同時(shí)也會(huì)對(duì)一些常見(jiàn)的php函數(shù)進(jìn)行介紹。

php函數(shù)的分類

在php中,橫向劃分的話,函數(shù)分為兩大類: user function(用戶函數(shù)) 和internal function(內(nèi)置函數(shù))。
前者就是用戶在程序中自定義的一些函數(shù)和方法,后者則是php本身提供的各類庫(kù)函數(shù)(比如sprintf、array_push等)。
用戶也可以通過(guò)擴(kuò)展的方法來(lái)編寫庫(kù)函數(shù),這個(gè)將在后面介紹。
對(duì)于user function,又可以細(xì)分為function(函數(shù))和method(類方法),本文中將就這三種函數(shù)分別進(jìn)行分析和測(cè)試。

php函數(shù)的實(shí)現(xiàn)

一個(gè)php函數(shù)最終是如何執(zhí)行,這個(gè)流程是怎么樣的呢?

要回答這個(gè)問(wèn)題,我們先來(lái)看看php代碼的執(zhí)行所經(jīng)過(guò)的流程。

從圖1可以看到,php實(shí)現(xiàn)了一個(gè)典型的動(dòng)態(tài)語(yǔ)言執(zhí)行過(guò)程:拿到一段代碼后,經(jīng)過(guò)詞法解析、語(yǔ)法解析等階段后,
源程序會(huì)被翻譯成一個(gè)個(gè)指令(opcodes),然后ZEND虛擬機(jī)順次執(zhí)行這些指令完成操作。Php本身是用c實(shí)現(xiàn)的,
因此最終調(diào)用的也都是c的函數(shù),實(shí)際上,我們可以把php看做是一個(gè)c開(kāi)發(fā)的軟件。
通過(guò)上面描述不難看出,php中函數(shù)的執(zhí)行也是被翻譯成了opcodes來(lái)調(diào)用,每次函數(shù)調(diào)用實(shí)際上是執(zhí)行了一條或多條指令。

對(duì)于每一個(gè)函數(shù),zend都通過(guò)以下的數(shù)據(jù)結(jié)構(gòu)來(lái)描述

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標(biāo)明了函數(shù)的類型:用戶函數(shù)、內(nèi)置函數(shù)、重載函數(shù)。
Common中包含函數(shù)的基本信息,包括函數(shù)名,參數(shù)信息,
函數(shù)標(biāo)志(普通函數(shù)、靜態(tài)方法、抽象方法)等內(nèi)容。
另外,對(duì)于用戶函數(shù),還有一個(gè)函數(shù)符號(hào)表,記錄了內(nèi)部變量等,這個(gè)將在后面詳述。

Zend維護(hù)了一個(gè)全局function_table,這是一個(gè)大的hash表。函數(shù)調(diào)用的時(shí)候會(huì)首先根據(jù)函數(shù)名從表中找到對(duì)應(yīng)的zend_function。
當(dāng)進(jìn)行函數(shù)調(diào)用時(shí)候,虛擬機(jī)會(huì)根據(jù)type的不同決定調(diào)用方法, 不同類型的函數(shù),其執(zhí)行原理是不相同的

內(nèi)置函數(shù)

內(nèi)置函數(shù),其本質(zhì)上就是真正的c函數(shù),每一個(gè)內(nèi)置函數(shù),php在最終編譯后都會(huì)展開(kāi)成為一個(gè)名叫zif_xxxx的function,比如我們常見(jiàn)的sprintf,
對(duì)應(yīng)到底層就是zif_sprintf。Zend在執(zhí)行的時(shí)候,如果發(fā)現(xiàn)是內(nèi)置函數(shù),則只是簡(jiǎn)單的做一個(gè)轉(zhuǎn)發(fā)操作。

Zend提供了一系列的api供調(diào)用,包括參數(shù)獲取、數(shù)組操作、內(nèi)存分配等。內(nèi)置函數(shù)的參數(shù)獲取,通過(guò)zend_parse_parameters方法來(lái)實(shí)現(xiàn),
對(duì)于數(shù)組、字符串等參數(shù),zend實(shí)現(xiàn)的是淺拷貝,因此這個(gè)效率是很高的??梢赃@樣說(shuō),對(duì)于php內(nèi)置函數(shù),其效率和相應(yīng)c函數(shù)幾乎相同,
唯一多了一次轉(zhuǎn)發(fā)調(diào)用。

內(nèi)置函數(shù)在php中都是通過(guò)so的方式進(jìn)行動(dòng)態(tài)加載,用戶也可以根據(jù)需要自己編寫相應(yīng)的so,也就是我們常說(shuō)的擴(kuò)展。
ZEND提供了一系列的api供擴(kuò)展使用,下面是mysqli_real_query函數(shù)的C函數(shù)實(shí)現(xiàn):
php-5.3.8\php-5.3.8\ext\mysqli\mysqli_api.c

PHP_FUNCTION(mysqli_real_query)
{
   MY_MYSQL *mysql;
   zval  *mysql_link;
   char  *query = NULL;
   int   query_len;

   if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, getThis(), "Os", &mysql_link, mysqli_link_class_entry, &query, &query_len) == FAILURE) {
      return;
   }
   MYSQLI_FETCH_RESOURCE_CONN(mysql, &mysql_link, MYSQLI_STATUS_VALID);

   MYSQLI_DISABLE_MQ; /* disable multi statements/queries */

   if (mysql_real_query(mysql->mysql, query, query_len)) {
      MYSQLI_REPORT_MYSQL_ERROR(mysql->mysql);
      RETURN_FALSE;
   }

   if (!mysql_field_count(mysql->mysql)) {
      if (MyG(report_mode) & MYSQLI_REPORT_INDEX) {
         php_mysqli_report_index(query, mysqli_server_status(mysql->mysql) TSRMLS_CC);
      }
   }

   RETURN_TRUE;
}

用戶函數(shù)

和內(nèi)置函數(shù)相比,用戶通過(guò)php實(shí)現(xiàn)的自定義函數(shù)具有完全不同的執(zhí)行過(guò)程和實(shí)現(xiàn)原理。如前文所述,
我們知道php代碼是被翻譯成為了一條條opcode來(lái)執(zhí)行的,用戶函數(shù)也不例外,實(shí)際中每個(gè)函數(shù)對(duì)應(yīng)到一組opcode,
這組指令被保存在zend_function中。于是,用戶函數(shù)的調(diào)用最終就是對(duì)應(yīng)到一組opcodes的執(zhí)行。

  • 局部變量的保存及遞歸的實(shí)現(xiàn)
    我們知道,函數(shù)遞歸是通過(guò)堆棧來(lái)完成的。在php中,也是利用類似的方法來(lái)實(shí)現(xiàn)。Zend為每個(gè)php函數(shù)分配了一個(gè)活動(dòng)符號(hào)表(active_sym_table),
    記錄當(dāng)前函數(shù)中所有局部變量的狀態(tài)。所有的符號(hào)表通過(guò)堆棧的形式來(lái)維護(hù),每當(dāng)有函數(shù)調(diào)用的時(shí)候,分配一個(gè)新的符號(hào)表并入棧。
    當(dāng)調(diào)用結(jié)束后當(dāng)前符號(hào)表出棧。由此實(shí)現(xiàn)了狀態(tài)的保存和遞歸。

對(duì)于棧的維護(hù),zend在這里做了優(yōu)化。預(yù)先分配一個(gè)長(zhǎng)度為N的靜態(tài)數(shù)組來(lái)模擬堆棧,
這種通過(guò)靜態(tài)數(shù)組來(lái)模擬動(dòng)態(tài)數(shù)據(jù)結(jié)構(gòu)的手法在我們自己的程序中也經(jīng)常有使用,這種方式避免了每次調(diào)用帶來(lái)的內(nèi)存分配、銷毀。
ZEND只是在函數(shù)調(diào)用結(jié)束時(shí)將當(dāng)前棧頂?shù)姆?hào)表數(shù)據(jù)clean掉即可。
因?yàn)殪o態(tài)數(shù)組長(zhǎng)度為N,一旦函數(shù)調(diào)用層次超過(guò)N,程序不會(huì)出現(xiàn)棧溢出,這種情況下zend就會(huì)進(jìn)行符號(hào)表的分配、銷毀,因此會(huì)導(dǎo)致性能下降很多。
在zend里面,N目前取值是32。因此,我們編寫php程序的時(shí)候,函數(shù)調(diào)用層次最好不要超過(guò)32。
當(dāng)然,如果是web應(yīng)用,本身可以函數(shù)調(diào)用層次的深度。

  • 參數(shù)的傳遞
    和內(nèi)置函數(shù)調(diào)用zend_parse_params來(lái)獲取參數(shù)不同,用戶函數(shù)中參數(shù)的獲取是通過(guò)指令來(lái)完成的。函數(shù)有幾個(gè)參數(shù)就對(duì)應(yīng)幾條指令。
    具體到實(shí)現(xiàn)上就是普通的變量賦值。
    通過(guò)上面的分析可以看出,和內(nèi)置函數(shù)相比,由于是自己維護(hù)堆棧表,而且每條指令的執(zhí)行也是一個(gè)c函數(shù),用戶函數(shù)的性能相對(duì)會(huì)差很多,
    后面會(huì)有具體的對(duì)比分析。因此,如果一個(gè)功能有對(duì)應(yīng)php內(nèi)置函數(shù)實(shí)現(xiàn)的盡量不要自己重新寫函數(shù)去實(shí)現(xiàn)。
類方法

類方法其執(zhí)行原理和用戶函數(shù)是相同的,也是翻譯成opcodes順次調(diào)用。類的實(shí)現(xiàn),zend用一個(gè)數(shù)據(jù)結(jié)構(gòu)zend_class_entry來(lái)實(shí)現(xiàn),
里面保存了類相關(guān)的一些基本信息。這個(gè)entry是在php編譯的時(shí)候就已經(jīng)處理完成。

在zend_function的common中,有一個(gè)成員叫做scope,其指向的就是當(dāng)前方法對(duì)應(yīng)類的zend_class_entry。關(guān)于php中面向?qū)ο蟮膶?shí)現(xiàn),
這里就不在做更詳細(xì)的介紹,今后將專門寫一篇文章來(lái)詳述php中面向?qū)ο蟮膶?shí)現(xiàn)原理。就函數(shù)這一塊來(lái)說(shuō),method實(shí)現(xiàn)原理和function完全相同,
理論上其性能也差不多,后面我們將做詳細(xì)的性能對(duì)比。

性能對(duì)比

函數(shù)名長(zhǎng)度對(duì)性能的影響
  • 測(cè)試方法
    對(duì)名字長(zhǎng)度為1、2、4、8、16的函數(shù)進(jìn)行比較,測(cè)試比較它們每秒可執(zhí)行次數(shù),確定函數(shù)名長(zhǎng)度對(duì)性能的影響
  • 測(cè)試結(jié)果如下圖

       

  • 結(jié)果分析
    從圖上可以看出,函數(shù)名的長(zhǎng)度對(duì)性能還是會(huì)有一定的影響。一個(gè)長(zhǎng)度為1的函數(shù)和長(zhǎng)度為16的 空函數(shù)調(diào)用 ,其性能差了1倍。
    分析一下源碼不難找到原因,如前面敘述所說(shuō),函數(shù)調(diào)用的時(shí)候zend會(huì)先在一個(gè)全局的funtion_table中通過(guò)函數(shù)名查詢相關(guān)信息,
    function_table是一個(gè)哈希表。必然的,名字越長(zhǎng)查詢所需要的時(shí)間就越多。
    因此,在實(shí)際編寫程序的時(shí)候,對(duì)多次調(diào)用的函數(shù),名字建議不要太長(zhǎng)

雖然函數(shù)名長(zhǎng)度對(duì)性能有一定影響,但具體有多大呢?這個(gè)問(wèn)題應(yīng)該還是需要結(jié)合實(shí)際情況來(lái)考慮,如果一個(gè)函數(shù)本身比較復(fù)雜的話,
那么對(duì)整體的性能影響并不大。
一個(gè)建議是對(duì)于那些會(huì)調(diào)用很多次,本身功能又比較簡(jiǎn)單的函數(shù),可以適當(dāng)取一些言簡(jiǎn)意賅的名字。

函數(shù)個(gè)數(shù)對(duì)性能的影響
  • 測(cè)試方法
    在以下三種環(huán)境下進(jìn)行函數(shù)調(diào)用測(cè)試,分析結(jié)果:1.程序僅包含1個(gè)函數(shù) 2.程序包含100個(gè)函數(shù) 3.程序包含1000個(gè)函數(shù)。
    測(cè)試這三種情況下每秒所能調(diào)用的函數(shù)次數(shù)
  • 測(cè)試結(jié)果如下圖

       

  • 結(jié)果分析
    從測(cè)試結(jié)果可以看出,這三種情況下性能幾乎相同,函數(shù)個(gè)數(shù)增加時(shí)性能下降微乎其微,可以忽略。
    從實(shí)現(xiàn)原理分析,幾種實(shí)現(xiàn)下唯一的區(qū)別在于函數(shù)獲取的部分。如前文所述,所有的函數(shù)都放在一個(gè)hash表中,
    在不同個(gè)數(shù)下查找效率都應(yīng)該還是接近于O(1),所以性能差距不大。
不同類型函數(shù)調(diào)用消耗
  • 測(cè)試方法
    選取用戶函數(shù)、類方法、靜態(tài)方法、內(nèi)置函數(shù)各一種,函數(shù)本身不做任何事情,直接返回,主要測(cè)試空函數(shù)調(diào)用的消耗。
    測(cè)試結(jié)果為每秒可執(zhí)行次數(shù)
    測(cè)試中為去除其他影響,所有函數(shù)名字長(zhǎng)度相同
  • 測(cè)試結(jié)果如下圖

       

  • 結(jié)果分析
    通過(guò)測(cè)試結(jié)果可以看到,對(duì)于用戶自己編寫的php函數(shù),不管是哪種類型,其效率是差不多的,均在280w/s左右。如我們預(yù)期,即使是空調(diào),
    內(nèi)置函數(shù)其效率也要高很多,達(dá)到780w/s,是前者是3倍。可見(jiàn),內(nèi)置函數(shù)調(diào)用的開(kāi)銷還是遠(yuǎn)低于用戶函數(shù)。
    從前面原理分析可知主要差距在于用戶函數(shù)調(diào)用時(shí)初始化符號(hào)表、接收參數(shù)等操作。
內(nèi)置函數(shù)和用戶函數(shù)性能對(duì)比
  • 測(cè)試方法
    內(nèi)置函數(shù)和用戶函數(shù)的性能對(duì)比,這里我們選取幾個(gè)常用的函數(shù),然后用php實(shí)現(xiàn)相同功能的函數(shù)進(jìn)行一下性能對(duì)比。
    測(cè)試中,我們選取字符串、數(shù)學(xué)、數(shù)組中各一個(gè)典型進(jìn)行對(duì)比,這幾個(gè)函數(shù)分別是字符串截取(substr)、10進(jìn)制轉(zhuǎn)2進(jìn)制(decbin)、
    求最小值(min)和返回?cái)?shù)組中的所以key(array_keys)。
  • 測(cè)試結(jié)果如下圖

       

  • 結(jié)果分析
    從測(cè)試結(jié)果可以看出,如我們預(yù)期,內(nèi)置函數(shù)在總體性能上遠(yuǎn)高于普通用戶函數(shù)。尤其對(duì)于涉及到字符串類操作的函數(shù),
    差距達(dá)到了1個(gè)數(shù)量級(jí)。因此,函數(shù)使用的一個(gè)原則就是如果某功能有相應(yīng)的內(nèi)置函數(shù),盡量使用它而不是自己編寫php函數(shù)。
    對(duì)于一些涉及到大量字符串操作的功能,為提高性能,可以考慮用擴(kuò)展來(lái)實(shí)現(xiàn)。比如常見(jiàn)的富文本過(guò)濾等。
和C函數(shù)性能對(duì)比
  • 測(cè)試方法
    我們選取字符串操作和算術(shù)運(yùn)算各3種函數(shù)進(jìn)行比對(duì),php用擴(kuò)展實(shí)現(xiàn)。三種函數(shù)是簡(jiǎn)單的一次算法運(yùn)算、字符串比較和多次的算法運(yùn)算。
    除了本身的兩類函數(shù)外,還會(huì)測(cè)試將函數(shù)空調(diào)開(kāi)銷去掉后的性能,一方面比對(duì)一下兩種函數(shù)(c和php內(nèi)置)本身的性能差異,
    另外就是側(cè)面印證空調(diào)函數(shù)的消耗
    測(cè)試點(diǎn)為執(zhí)行10w次操作的時(shí)間消耗
  • 測(cè)試結(jié)果如下圖

       

  • 結(jié)果分析
    內(nèi)置函數(shù)和C函數(shù)的開(kāi)銷在去掉php函數(shù)空調(diào)用的影響后差距較小,隨著函數(shù)功能越來(lái)越復(fù)雜,雙方性能趨近于相同。
    這個(gè)從之前的函數(shù)實(shí)現(xiàn)分析中也容易得到論證,畢竟內(nèi)置函數(shù)就是C實(shí)現(xiàn)的。
    函數(shù)功能越復(fù)雜,c和php的性能差距越小
    相對(duì)c來(lái)說(shuō),php函數(shù)調(diào)用的開(kāi)銷大很多,對(duì)于簡(jiǎn)單函數(shù)來(lái)說(shuō)性能還是有一定影響。因此php中函數(shù)不宜嵌套封裝太深。

偽函數(shù)及其性能

在php中,有這樣一些函數(shù),它們?cè)谑褂蒙鲜菢?biāo)準(zhǔn)的函數(shù)用法,但底層實(shí)現(xiàn)卻和真正函數(shù)調(diào)用完全不同,這些函數(shù)不屬于前文提到的三種function中的任何一類,
其實(shí)質(zhì)是一條單獨(dú)的opcode,這里估且叫做偽函數(shù)或者指令函數(shù)。

如上所說(shuō),偽函數(shù)使用起來(lái)和標(biāo)準(zhǔn)的函數(shù)并無(wú)二致,看起來(lái)具有相同的特征。但是他們最終執(zhí)行的時(shí)候是被zend反映成了一條對(duì)應(yīng)的指令(opcode)來(lái)調(diào)用,
因此其實(shí)現(xiàn)更接近于if、for、算術(shù)運(yùn)算等操作。

  • php中的偽函數(shù)
    isset
    empty
    unset
    eval

通過(guò)上面的介紹可以看出,偽函數(shù)由于被直接翻譯成指令來(lái)執(zhí)行,和普通函數(shù)相比少了一次函數(shù)調(diào)用所帶來(lái)的開(kāi)銷,因此性能會(huì)更好一些。
我們通過(guò)如下測(cè)試來(lái)做一個(gè)對(duì)比。 Array_key_exists和isset兩者都可以判斷數(shù)組中某個(gè)key是否存在,看一下他們的性能

       

從圖上可以看出,和array_key_exists相比,isset性能要高出很多,基本是前者的4倍左右,而即使是和空函數(shù)調(diào)用相比,
其性能也要高出1倍左右。由此也側(cè)面印證再次說(shuō)明了php函數(shù)調(diào)用的開(kāi)銷還是比較大的。

總結(jié)及建議

通過(guò)對(duì)函數(shù)實(shí)現(xiàn)的原理分析和性能測(cè)試,我們總結(jié)出以下一些結(jié)論

1. Php的函數(shù)調(diào)用開(kāi)銷相對(duì)較大。

2. 函數(shù)相關(guān)信息保存在一個(gè)大的hash_table中,每次調(diào)用時(shí)通過(guò)函數(shù)名在hash表中查找,因此函數(shù)名長(zhǎng)度對(duì)性能也有一定影響。

3. 函數(shù)返回引用沒(méi)有實(shí)際意義

4. 內(nèi)置php函數(shù)性能比用戶函數(shù)高很多,尤其對(duì)于字符串類操作。

5. 類方法、普通函數(shù)、靜態(tài)方法效率幾乎相同,沒(méi)有太大差異

6. 除去空函數(shù)調(diào)用的影響,內(nèi)置函數(shù)和同樣功能的C函數(shù)性能基本差不多。

7. 所有的參數(shù)傳遞都是采用引用計(jì)數(shù)的淺拷貝,代價(jià)很小。

8. 函數(shù)個(gè)數(shù)對(duì)性能影響幾乎可以忽略

因此,對(duì)于php函數(shù)的使用,有如下一些建議

1. 一個(gè)功能可以用內(nèi)置函數(shù)完成,盡量使用它而不是自己編寫php函數(shù)。

2. 如果某個(gè)功能對(duì)性能要求很高,可以考慮用擴(kuò)展來(lái)實(shí)現(xiàn)。

3. Php函數(shù)調(diào)用開(kāi)銷較大,因此不要過(guò)分封裝。有些功能,如果需要調(diào)用的次數(shù)很多本身又只用1、2行代碼就行實(shí)現(xiàn)的,建議就不要封裝調(diào)用了。

4. 不要過(guò)分迷戀各種設(shè)計(jì)模式,如上一條描述,過(guò)分的封裝會(huì)帶來(lái)性能的下降。需要考慮兩者的權(quán)衡。Php有自己的特點(diǎn),
切不可東施效顰,過(guò)分效仿java的模式。

5. 函數(shù)不宜嵌套過(guò)深,遞歸使用要謹(jǐn)慎。

6. 偽函數(shù)性能很高,同等功能實(shí)現(xiàn)下優(yōu)先考慮。比如用isset代替array_key_exists

7. 函數(shù)返回引用沒(méi)有太大意義,也起不到實(shí)際作用,建議不予考慮。

8. 類成員方法效率不比普通函數(shù)低,因此不用擔(dān)心性能損耗。建議多考慮靜態(tài)方法,可讀性及安全性都更好。

9. 如不是特殊需要,參數(shù)傳遞都建議使用傳值而不是傳引用。當(dāng)然,如果參數(shù)是很大的數(shù)組且需要修改時(shí)可以考慮引用傳遞。