(转)PHP 函数的实现原理及性能分析

前言

任何语言中,函数都是最基本的组成单元。对于php的函数,它具备哪些特色?函数调用是怎么实现的?php函数的性能如何,有什么使用建议?本文 将从原理出发进行分析结合实际的性能测试尝试对这些问题进行回答,在了解实现的同时更好的编写php程序。同时也会对一些常见的php函数进行介绍。

php函数的分类

在php中,横向划分的话,函数分为两大类: user function(内置函数) 和internal function(内置函数)。前者就是用户在程序中自定义的一些函数和方法,后者则是php自己提供的各种库函数(好比sprintf、 array_push等)。用户也能够经过扩展的方法来编写库函数,这个将在后面介绍。对于user function,又能够细分为function(函数)和method(类方法),本文中将就这三种函数分别进行分析和测试。

php函数的实现

一个php函数最终是如何执行,这个流程是怎么样的呢?php

要回答这个问题,咱们先来看看php代码的执行所通过的流程。java

 

 

从图1能够看到,php实现了一个典型的动态语言执行过程:拿到一段代码后,通过词法解析、语法解析等阶段后,源程序会被翻译成一个个指令 (opcodes),而后ZEND虚拟机顺次执行这些指令完成操做。Php自己是用c实现的,所以最终调用的也都是c的函数,实际上,咱们能够把php看 作是一个c开发的软件。
经过上面描述不难看出,php中函数的执行也是被翻译成了opcodes来调用,每次函数调用其实是执行了一条或多条指令。 c++

对于每个函数,zend都经过如下的数据结构来描述 web

 

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供扩展使用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函数调用的开销仍是比较大的。

 

经常使用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. 如不是特殊须要,参数传递都建议使用传值而不是传引用。固然,若是参数是很大的数组且须要修改时能够考虑引用传递。

相关文章
相关标签/搜索