作者:随洋恒黯的天使 | 来源:互联网 | 2024-11-16 19:21
在任何编程语言中,函数都是最基本的组成单元。对于PHP函数,它有哪些特点?函数调用是如何实现的?PHP函数的性能如何?本文将从原理出发,结合实际性能测试,对这些问题进行详细解答,并介绍一些常见的PHP函数。
PHP函数主要分为两大类:用户函数(User Function)和内置函数(Internal Function)。用户函数是由开发者在程序中自定义的函数和方法,而内置函数则是PHP提供的各种库函数,如sprintf、array_push等。用户也可以通过扩展的方式编写库函数。
PHP函数的实现原理
PHP代码的执行过程包括词法解析、语法解析等阶段,最终被翻译成一系列指令(opcodes),由ZEND虚拟机依次执行。PHP本身是用C语言实现的,因此最终调用的也是C函数。每个PHP函数的执行流程都可以通过以下数据结构来描述:
typedef union _zend_function {
zend_uchar type;
struct {
zend_uchar type;
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
中包含函数的基本信息,如函数名、参数信息、函数标志等。PHP维护了一个全局的function_table
,这是一个大的哈希表,函数调用时会根据函数名从表中找到对应的zend_function
。
内置函数的实现
内置函数本质上是C函数,每个内置函数在PHP编译后都会展开为一个名为zif_xxxx
的函数,例如sprintf
对应的是zif_sprintf
。ZEND在执行时,如果发现是内置函数,只需简单地进行一次转发操作。内置函数的参数获取通过zend_parse_parameters
方法实现,对于数组、字符串等参数,PHP实现的是浅拷贝,因此效率较高。内置函数的性能与相应的C函数几乎相同,只是多了一次转发调用。
用户函数的实现
用户函数的执行过程与内置函数不同。PHP代码被翻译成一系列opcodes,用户函数也是如此。每个用户函数对应一组opcodes,这组指令被保存在zend_function
中。用户函数的调用最终就是执行这组opcodes。局部变量的保存和递归的实现通过堆栈来完成。ZEND为每个函数分配了一个活动符号表(active_sym_table),记录当前函数中所有局部变量的状态。所有的符号表通过堆栈形式维护,每当有函数调用时,分配一个新的符号表并入栈,调用结束后当前符号表出栈。为了优化性能,ZEND预先分配了一个长度为N的静态数组来模拟堆栈,避免了每次调用带来的内存分配和销毁。N目前取值为32,因此函数调用层次最好不要超过32。
类方法的实现
类方法的执行原理与用户函数相同,也是翻译成opcodes顺序调用。类的实现通过zend_class_entry
数据结构来实现,其中保存了类的相关信息。类方法的scope
成员指向当前方法对应的类的zend_class_entry
。类方法的实现原理和普通函数相同,理论上性能也差不多。
性能对比
函数名长度对性能的影响
测试结果显示,函数名的长度对性能有一定的影响。一个长度为1的函数和长度为16的空函数调用,性能相差约1倍。原因是函数调用时,ZEND会先在全局的function_table
中通过函数名查询相关信息,function_table
是一个哈希表,函数名越长,查询所需时间越多。因此,建议对多次调用的函数,名字不要过长。
函数个数对性能的影响
测试结果显示,函数个数增加时性能下降微乎其微,可以忽略。所有函数都放在一个哈希表中,查找效率接近于O(1),因此性能差距不大。
不同类型函数调用的消耗
测试结果显示,用户函数(包括类方法和静态方法)的效率大致相同,约为280万次/秒。内置函数的效率远高于用户函数,约为780万次/秒。内置函数的性能优势主要在于初始化符号表和接收参数等操作。
内置函数与用户函数的性能对比
测试结果显示,内置函数在总体性能上远高于普通用户函数,尤其是涉及字符串操作的函数,差距可达一个数量级。因此,如果某功能有相应的内置函数,尽量使用它而不是自己编写PHP函数。对于涉及大量字符串操作的功能,可以考虑用扩展来实现。
伪函数及其性能
PHP中有一些函数在使用上是标准的函数用法,但底层实现却和真正函数调用完全不同,这些函数被称为伪函数或指令函数。伪函数最终被翻译成一条对应的指令(opcode)来执行,因此性能更好。例如,isset
和array_key_exists
都可以判断数组中某个键是否存在,但isset
的性能要高出很多,基本是前者的4倍左右。
总结及建议
通过对函数实现原理的分析和性能测试,我们得出以下结论:
- PHP的函数调用开销相对较大。
- 函数相关信息保存在一个大的哈希表中,每次调用时通过函数名在哈希表中查找,因此函数名长度对性能有一定影响。
- 函数返回引用没有实际意义。
- 内置PHP函数性能比用户函数高很多,尤其对于字符串类操作。
- 类方法、普通函数、静态方法效率几乎相同,没有太大差异。
- 除去空函数调用的影响,内置函数和同样功能的C函数性能基本差不多。
- 所有参数传递都是采用引用计数的浅拷贝,代价很小。
- 函数个数对性能影响几乎可以忽略。
因此,对于PHP函数的使用,有以下建议:
- 一个功能可以用内置函数完成,尽量使用它而不是自己编写PHP函数。
- 如果某个功能对性能要求很高,可以考虑用扩展来实现。
- PHP函数调用开销较大,因此不要过分封装。有些功能,如果需要调用的次数很多,本身又只用1、2行代码就能实现的,建议不要封装调用。
- 不要过分迷恋各种设计模式,过分的封装会带来性能下降。需要考虑两者的权衡。PHP有自己的特点,切不可盲目效仿Java的模式。
- 函数不宜嵌套过深,递归使用要谨慎。
- 伪函数性能很高,同等功能实现下优先考虑。例如,用
isset
代替array_key_exists
。
- 函数返回引用没有太大意义,也起不到实际作用,建议不予考虑。
- 类成员方法效率不比普通函数低,因此不用担心性能损耗。建议多考虑静态方法,可读性和安全性都更好。
- 如不是特殊需要,参数传递都建议使用传值而不是传引用。当然,如果参数是很大的数组且需要修改时可以考虑引用传递。