首頁  >  文章  >  後端開發  >  php函數原理

php函數原理

angryTom
angryTom原創
2019-08-23 09:44:133004瀏覽

php函數原理

前言 

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

php函數的分類法

  在php中,橫向分割的話,函數分成兩大類: user function(內建函數) 和internal function(內建函數)。前者是使用者在程式中自訂的一些函數和方法,後者則是php本身提供的各類別函式庫函數(例如sprintf、 array_push等)。使用者也可以透過擴展的方法來編寫庫函數,這個將在後面介紹。對於user function,又可以細分為function(函數)和method(類別方法),本文將就這三種函數分別進行分析和測試。

推薦教學:PHP影片教學

#php函數的實作 

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

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

php函數原理

  從圖中可以看到,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,這是一個大的hahs表。函數呼叫的時候會先根據函數名稱從表中找到對應的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供擴展使用

用戶函數

#  和內建函數相比,使用者透過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的函數進行比較,測試比較它們每秒可執行次數,確定函數名稱長度對效能的影響

##測試結果如下圖

php函數原理

結果分析

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

  雖然函數名稱長度對效能有一定影響,但具體有多大呢?這個問題應該還是需要結合實際情況來考慮,如果一個函數本身比較複雜的話,那麼對整體的效能影響並不大。

  一個建議是對於那些會呼叫很多次,本身功能又比較簡單的函數,可以適當取一些言簡意賅的名字。

函數個數對效能的影響

#測試方法

  在下列三種環境下進行函數呼叫測試,分析結果:1.程式僅包含1個函數2.程式包含100個函數3.程式包含1000個函數。

  測試這三種情況下每秒所能呼叫的函數次數

#測試結果如下圖

php函數原理

結果分析

  從測試結果可以看出,這三種情況下效能幾乎相同,函數個數增加時效能下降微乎其微,可以忽略。

  從實現原理分析,幾種實現下唯一的差異在於函數獲取的部分。如前文所述,所有的函數都放在一個hash表中,在不同個數下查找效率都應該還是接近O(1),所以效能差距不大。

不同類型函數呼叫消耗

#測試方法

  選取使用者函數、類別方法、靜態方法、內建函數各一種,函數本身不做任何事情,直接傳回,主要測試空函數呼叫的消耗。測試結果為每秒可執行次數

  測試中為去除其他影響,所有函數名字長度相同

測試結果如下圖

php函數原理

結果分析

  透過測試結果可以看到,對於使用者自己寫的php函數,不管是哪一種類型,其效率是差不多的,均在280w/s左右。如我們預 期,即使是空調,內建函數其效率也要高很多,達到780w/s,是前者是3倍。可見,內建函數呼叫的開銷還是遠低於使用者函數。從前面原理分析可知主要差距 在於用戶函數呼叫時初始化符號表、接收參數等操作。

內建函數與使用者函數效能比較

#測試方法

  內建函數與使用者函數的效能對比,這裡我們選取幾個常用的函數,然後用php實作相同功能的函數來進行一下效能對比。

  測試中,我們選取字串、數學、陣列中各一個典型進行對比,這幾個函數分別是字串截取(substr)、10進位轉2進位(decbin)、求最小值(min)和傳回數組中的所以key(array_keys)。

測試結果如下圖

php函數原理

#結果分析

  從測試結果可以看出,如我們預期,內建函數在整體效能上遠高於一般使用者函數。尤其是涉及字串類別操作的函數,差距達到了1個數量級。因此,函數使用的一個原則就是如果某項功能有對應的內建函數,盡量使用它而不是自己寫php函數。

  對於一些涉及大量字串操作的功能,為提高效能,可以考慮用擴充來實現。例如常見的富文本過濾等。

  與C函數效能比較

測試方法

  我們選取字串運算與算術運算各3種函數進行比對,php用擴展實現。三種函數是簡單的一次演算法運算、字串比較和多次的演算法運算。

  除了本身的兩類函數外,還會測試將函數空調開銷去掉後的性能,一方面比對一下兩種函數(c和php內置)本身的性能差異,另外就是側面印證空調函數的消耗

  測試點為執行10w次操作的時間消耗

測試結果如下圖

php函數原理

結果分析

  內建函數和C函數的開銷在去掉php函數空調用的影響後差距較小,隨著函數功能越來越複雜,雙方性能趨近於相同。這個從之前的函數實作分析也容易得到論證,畢竟內建函數就是C實現的。

  函數功能越複雜,c和php的效能差距越小

  相對c來說,php函數呼叫的開銷大很多,對於簡單函數來說效能還是有一定影響。因此php中函數不宜嵌套封裝太深。

偽函數及其性能 

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

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

php中的偽函數

  1、isset

  2、empty

#  3、unset

# 4、eval

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

php函數原理

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

以上是php函數原理的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn