前言 php
在任何語言中,函數都是最基本的技術單元之一。對於php的函數,它具備哪些特色?函數調用是怎麼實現?php函數的性能如何,有什麼使用建議?本文將從原理出發進行分析結合實際的性能測試嘗試對這些問題進行回答,在瞭解實現的同時更好的編寫php程序。同時也會對一些常見的php函數進行介紹。
html
php函數的分類 java
在php中,橫向劃分的話,函數分爲兩大類: user function(內置函數) 和 internal function(內置函數)。前者就是用戶在程序中自定義的一些函數和方法,後者則是php自己提供的各種庫函數(好比sprintf、 array_push等)。用戶也能夠經過擴展的方法來編寫庫函數,這個將在後面介紹。對於user function,又能夠細分爲function(函數)和method(類方法),本文中將就這三種函數分別進行分析和測試。
c++
php函數的實現 web
一個php函數最終是如何執行,這個流程是怎麼樣的呢?
要回答這個問題,咱們先來看看php代碼的執行所通過的流程。 算法
從圖1能夠看到,php實現了一個典型的動態語言執行過程:拿到一段代碼後,通過詞法解析、語法解析等階段後,源程序會被翻譯成一個個指令 (opcodes),而後ZEND虛擬機順次執行這些指令完成操做。Php自己是用c實現的,所以最終調用的也都是c的函數,實際上,咱們能夠把php看 作是一個c開發的軟件。 設計模式
經過上面描述不難看出,php中函數的執行也是被翻譯成了opcodes來調用,每次函數調用其實是執行了一條或多條指令。 api
對於每個函數,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徹底相同,理論上其性能也差很少,後面咱們將作詳細的性能對比。
性能對比
函數名長度對性能的影響
測試方法
對名字長度爲一、二、四、八、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函數調用的開銷仍是比較大的。本文地址:http://3aj.cn/php/68.html
經常使用php函數實現及介紹
count
count是咱們常常用到的一個函數,其功能是返回一個數組的長度。
count這個函數,其複雜度是多少呢?
一種常見的說法是count函數會遍歷整個數組而後求出元素個數,所以複雜度是O(n)。那實際狀況是否是這樣呢?
咱們回到count的實現來看一下,經過源碼能夠發現,對於數組的count操做,函數最終的路徑是zif_count-> php_count_recursive-> zend_hash_num_elements,而zend_hash_num_elements的行爲是 return ht->nNumOfElements,可見,這是一個O(1)而不是O(n)的操做。實際上,數組在php底層就是一個hash_table,對 於hash表,zend中專門有一個元素nNumOfElements記錄了當前元素的個數,所以對於通常的count實際上直接就返回了這個值。由此, 咱們得出結論: count是O(1)的複雜度,和具體數組的大小無關。
非數組類型的變量,count的行爲時怎樣?
對於未設置變量返回0,而像int、double、string等則會返回1
strlen
Strlen用於返回一個字符串的長度。那麼,他的實現原理是如何的呢?
咱們都知道在c中strlen是一個o(n)的函數,會順序遍歷字符串直到遇到/0,而後出長度。Php中是否也這樣呢?答案是否認的,php裏字符串是 用一個複合結構來描述,包括指向具體數據的指針和字符串長度(和c++中string相似),所以strlen就直接返回字符串長度了,是常數級別的操 做。
另外,對於非字符串類型的變量調用strlen,它會首先將變量強制轉換爲字符串再求長度,這點須要注意。
isset和array_key_exists
這兩個函數最多見的用法都是判斷一個key是否在數組中存在。可是前者還能夠用於判斷一個變量是否被設置過。
如前文所述,isset並不是真正的函數,所以它的效率會比後者高不少。推薦用它代替array_key_exists。
array_push和array[]
二者都是往數組尾部追加一個元素。不一樣的是前者能夠一次push多個。他們最大的區別在於一個是函數一個是語言結構,所以後者效率要更高。所以若是隻是普通的追加元素,建議使用array []。
rand和mt_rand
二者都是提供產生隨機數的功能,前者使用libc標準的rand。後者用了 Mersenne Twister 中已知的特性做爲隨機數發生器,它能夠產生隨機數值的平均速度比 libc 提供的 rand() 快四倍。所以若是對性能要求較高,能夠考慮用mt_rand代替前者。
咱們都知道,rand產生的是僞隨機數,在C中須要用srand顯示指定種子。可是在php中,rand會本身幫你默認調用一次srand,通常狀況下不須要本身再顯示的調用。
須要注意的是,若是特殊狀況下須要調用srand時,必定要配套調用。就是說srand對於rand,mt_srand對應srand,切不可混合使用,不然是無效的。
sort和usort
二者都是用於排序,不一樣的是前者能夠指定排序策略,相似咱們C裏面的qsort和C++的sort。
在排序上二者都是採用標準的快排來實現,對於有排序需求的,如非特殊狀況調用php提供的這些方法就能夠了,不用本身從新實現一遍,效率會低不少。緣由見前文對於用戶函數和內置函數的分析比對。
urlencode和rawurlencode
這兩個都是用於url編碼, 字符串中除了 -_. 以外的全部非字母數字字符都將被替換成百分號(%)後跟兩位十六進制數。二者惟一的區別在於對於空格,urlencode會編碼爲+,而rawurlencode會編碼爲%20。
通常狀況下除了搜索引擎,咱們的策略都是空格編碼爲%20。所以採用後者的居多。
注意的是encode和decode系列必定要配套使用。
strcmp系列函數
這一系列的函數包括strcmp、strncmp、strcasecmp、strncasecmp,實現功能和C函數相同。但也有不一樣,因爲php的字符串是容許/0出現,所以在判斷的時候底層使用的是memcmp系列而非strcmp,理論上來講更快。
另外因爲php直接能獲取到字符串長度,所以會首先這方面的檢查,不少狀況下效率就會高不少了。
is_int和is_numeric
這兩個函數功能類似又不徹底相同,使用的時候必定須要注意他們的區別。
Is_int:判斷一個變量類型是不是整數型,php變量中專門有一個字段表徵類型,所以直接判斷這個類型便可,是一個絕對O(1)的操做
Is_numeric:判斷一個變量是不是整數或數字字符串,也就是說除了整數型變量會返回true以外,對於字符串變量,若是形如」1234」,」1e4」等也會被判爲true。這個時候會遍歷字符串進行判斷。
總結及建議
經過對函數實現的原理分析和性能測試,咱們總結出如下一些結論
1. Php的函數調用開銷相對較大。
2. 函數相關信息保存在一個大的hash_table中,每次調用時經過函數名在hash表中查找,所以函數名長度對性能也有必定影響。
3. 函數返回引用沒有實際意義
4. 內置php函數性能比用戶函數高不少,尤爲對於字符串類操做。
5. 類方法、普通函數、靜態方法效率幾乎相同,沒有太大差別
6. 除去空函數調用的影響,內置函數和一樣功能的C函數性能基本差很少。
7. 全部的參數傳遞都是採用引用計數的淺拷貝,代價很小。
8. 函數個數對性能影響幾乎能夠忽略
所以,對於php函數的使用,有以下一些建議 1. 一個功能能夠用內置函數完成,儘可能使用它而不是本身編寫php函數。 2. 若是某個功能對性能要求很高,能夠考慮用擴展來實現。 3. Php函數調用開銷較大,所以不要過度封裝。有些功能,若是須要調用的次數不少自己又只用一、2行代碼就行實現的,建議就不要封裝調用了。 4. 不要過度迷戀各類設計模式,如上一條描述,過度的封裝會帶來性能的降低。須要考慮二者的權衡。Php有本身的特色,切不可東施效顰,過度效仿java的模式。 5. 函數不宜嵌套過深,遞歸使用要謹慎。 6. 僞函數性能很高,同等功能實現下優先考慮。好比用isset代替array_key_exists 7. 函數返回引用沒有太大意義,也起不到實際做用,建議不予考慮。 8. 類成員方法效率不比普通函數低,所以不用擔憂性能損耗。建議多考慮靜態方法,可讀性及安全性都更好。 9. 如不是特殊須要,參數傳遞都建議使用傳值而不是傳引用。固然,若是參數是很大的數組且須要修改時能夠考慮引用傳遞。