轉載出處:http://www.cnblogs.com/ghj1976/archive/2010/08/27/1809766.html

前言

在任何語言中,函數都是最基本的組成單元。對于php的函數,它具有哪些特點?函數調用是怎么實現的?php函數的性能如何,有什么使用建議?
本文將從原理出發進行分析結合實際的性能測試嘗試對這些問題進行回答,在了解實現的同時更好的編寫php程序。同時也會對一些常見的php函數進行介紹。

php函數的分類

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

php函數的實現

一個php函數最終是如何執行,這個流程是怎么樣的呢?

要回答這個問題,我們先來看看php代碼的執行所經過的流程。

從圖1可以看到,php實現了一個典型的動態語言執行過程:拿到一段代碼后,經過詞法解析、語法解析等階段后,
源程序會被翻譯成一個個指令(opcodes),然后ZEND虛擬機順次執行這些指令完成操作。Php本身是用c實現的,
因此最終調用的也都是c的函數,實際上,我們可以把php看做是一個c開發的軟件。
通過上面描述不難看出,php中函數的執行也是被翻譯成了opcodes來調用,每次函數調用實際上是執行了一條或多條指令。

對于每一個函數,zend都通過以下的數據結構來描述

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

Zend維護了一個全局function_table,這是一個大的hash表。函數調用的時候會首先根據函數名從表中找到對應的zend_function。
當進行函數調用時候,虛擬機會根據type的不同決定調用方法, 不同類型的函數,其執行原理是不相同的

內置函數

內置函數,其本質上就是真正的c函數,每一個內置函數,php在最終編譯后都會展開成為一個名叫zif_xxxx的function,比如我們常見的sprintf,
對應到底層就是zif_sprintf。Zend在執行的時候,如果發現是內置函數,則只是簡單的做一個轉發操作。

Zend提供了一系列的api供調用,包括參數獲取、數組操作、內存分配等。內置函數的參數獲取,通過zend_parse_parameters方法來實現,
對于數組、字符串等參數,zend實現的是淺拷貝,因此這個效率是很高的。可以這樣說,對于php內置函數,其效率和相應c函數幾乎相同,
唯一多了一次轉發調用。

內置函數在php中都是通過so的方式進行動態加載,用戶也可以根據需要自己編寫相應的so,也就是我們常說的擴展。
ZEND提供了一系列的api供擴展使用,下面是mysqli_real_query函數的C函數實現:
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;
}

用戶函數

和內置函數相比,用戶通過php實現的自定義函數具有完全不同的執行過程和實現原理。如前文所述,
我們知道php代碼是被翻譯成為了一條條opcode來執行的,用戶函數也不例外,實際中每個函數對應到一組opcode,
這組指令被保存在zend_function中。于是,用戶函數的調用最終就是對應到一組opcodes的執行。

  • 局部變量的保存及遞歸的實現
    我們知道,函數遞歸是通過堆棧來完成的。在php中,也是利用類似的方法來實現。Zend為每個php函數分配了一個活動符號表(active_sym_table),
    記錄當前函數中所有局部變量的狀態。所有的符號表通過堆棧的形式來維護,每當有函數調用的時候,分配一個新的符號表并入棧。
    當調用結束后當前符號表出棧。由此實現了狀態的保存和遞歸。

對于棧的維護,zend在這里做了優化。預先分配一個長度為N的靜態數組來模擬堆棧,
這種通過靜態數組來模擬動態數據結構的手法在我們自己的程序中也經常有使用,這種方式避免了每次調用帶來的內存分配、銷毀。
ZEND只是在函數調用結束時將當前棧頂的符號表數據clean掉即可。
因為靜態數組長度為N,一旦函數調用層次超過N,程序不會出現棧溢出,這種情況下zend就會進行符號表的分配、銷毀,因此會導致性能下降很多。
在zend里面,N目前取值是32。因此,我們編寫php程序的時候,函數調用層次最好不要超過32。
當然,如果是web應用,本身可以函數調用層次的深度。

  • 參數的傳遞
    和內置函數調用zend_parse_params來獲取參數不同,用戶函數中參數的獲取是通過指令來完成的。函數有幾個參數就對應幾條指令。
    具體到實現上就是普通的變量賦值。
    通過上面的分析可以看出,和內置函數相比,由于是自己維護堆棧表,而且每條指令的執行也是一個c函數,用戶函數的性能相對會差很多,
    后面會有具體的對比分析。因此,如果一個功能有對應php內置函數實現的盡量不要自己重新寫函數去實現。
類方法

類方法其執行原理和用戶函數是相同的,也是翻譯成opcodes順次調用。類的實現,zend用一個數據結構zend_class_entry來實現,
里面保存了類相關的一些基本信息。這個entry是在php編譯的時候就已經處理完成。

在zend_function的common中,有一個成員叫做scope,其指向的就是當前方法對應類的zend_class_entry。關于php中面向對象的實現,
這里就不在做更詳細的介紹,今后將專門寫一篇文章來詳述php中面向對象的實現原理。就函數這一塊來說,method實現原理和function完全相同,
理論上其性能也差不多,后面我們將做詳細的性能對比。

性能對比

函數名長度對性能的影響
  • 測試方法
    對名字長度為1、2、4、8、16的函數進行比較,測試比較它們每秒可執行次數,確定函數名長度對性能的影響
  • 測試結果如下圖

       

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

雖然函數名長度對性能有一定影響,但具體有多大呢?這個問題應該還是需要結合實際情況來考慮,如果一個函數本身比較復雜的話,
那么對整體的性能影響并不大。
一個建議是對于那些會調用很多次,本身功能又比較簡單的函數,可以適當取一些言簡意賅的名字。

函數個數對性能的影響
  • 測試方法
    在以下三種環境下進行函數調用測試,分析結果:1.程序僅包含1個函數 2.程序包含100個函數 3.程序包含1000個函數。
    測試這三種情況下每秒所能調用的函數次數
  • 測試結果如下圖

       

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

       

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

       

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

       

  • 結果分析
    內置函數和C函數的開銷在去掉php函數空調用的影響后差距較小,隨著函數功能越來越復雜,雙方性能趨近于相同。
    這個從之前的函數實現分析中也容易得到論證,畢竟內置函數就是C實現的。
    函數功能越復雜,c和php的性能差距越小
    相對c來說,php函數調用的開銷大很多,對于簡單函數來說性能還是有一定影響。因此php中函數不宜嵌套封裝太深。

偽函數及其性能

在php中,有這樣一些函數,它們在使用上是標準的函數用法,但底層實現卻和真正函數調用完全不同,這些函數不屬于前文提到的三種function中的任何一類,
其實質是一條單獨的opcode,這里估且叫做偽函數或者指令函數。

如上所說,偽函數使用起來和標準的函數并無二致,看起來具有相同的特征。但是他們最終執行的時候是被zend反映成了一條對應的指令(opcode)來調用,
因此其實現更接近于if、for、算術運算等操作。

  • php中的偽函數
    isset
    empty
    unset
    eval

通過上面的介紹可以看出,偽函數由于被直接翻譯成指令來執行,和普通函數相比少了一次函數調用所帶來的開銷,因此性能會更好一些。
我們通過如下測試來做一個對比。 Array_key_exists和isset兩者都可以判斷數組中某個key是否存在,看一下他們的性能

       

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

總結及建議

通過對函數實現的原理分析和性能測試,我們總結出以下一些結論

1. Php的函數調用開銷相對較大。

2. 函數相關信息保存在一個大的hash_table中,每次調用時通過函數名在hash表中查找,因此函數名長度對性能也有一定影響。

3. 函數返回引用沒有實際意義

4. 內置php函數性能比用戶函數高很多,尤其對于字符串類操作。

5. 類方法、普通函數、靜態方法效率幾乎相同,沒有太大差異

6. 除去空函數調用的影響,內置函數和同樣功能的C函數性能基本差不多。

7. 所有的參數傳遞都是采用引用計數的淺拷貝,代價很小。

8. 函數個數對性能影響幾乎可以忽略

因此,對于php函數的使用,有如下一些建議

1. 一個功能可以用內置函數完成,盡量使用它而不是自己編寫php函數。

2. 如果某個功能對性能要求很高,可以考慮用擴展來實現。

3. Php函數調用開銷較大,因此不要過分封裝。有些功能,如果需要調用的次數很多本身又只用1、2行代碼就行實現的,建議就不要封裝調用了。

4. 不要過分迷戀各種設計模式,如上一條描述,過分的封裝會帶來性能的下降。需要考慮兩者的權衡。Php有自己的特點,
切不可東施效顰,過分效仿java的模式。

5. 函數不宜嵌套過深,遞歸使用要謹慎。

6. 偽函數性能很高,同等功能實現下優先考慮。比如用isset代替array_key_exists

7. 函數返回引用沒有太大意義,也起不到實際作用,建議不予考慮。

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

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