php_function(count){ zval *array; long mode = count_normal; if (zend_parse_parameters(zend_num_args() tsrmls_cc, "z|l", &array, &mode) == failure) { return; } ....}
在php7以后,变成了:
php_function(count){ zval *array; zend_long mode = count_normal; zend_parse_parameters_start(1, 2) z_param_zval(array) z_param_optional z_param_long(mode) zend_parse_parameters_end(); ...}
很多php扩展开发的同学可能在初次接触的时候,会觉得很陌生,不要焦虑,让我慢慢道来 :)
当时在做phpng(php7的开发项目代号)的开发的时候,我们主要的发现性能提升点的一个方式就是bench各种大型实际项目,来发现占用资源比较大的部分,而最常用benchmark对象之一是wordpress,因为它够复杂,够慢,(它也是我们开发jit的时候对主要bench目标:)) 代表了非oo型代码类的典型应用, 在实际的benchmark的过程中我们发现,将近有6%的耗时被zend_parse_parameters给占用了。
事实上zend_parameters_parsing确实是一个很庞大的函数:
zend_api int zend_parse_parameters(int num_args, const char *type_spec, ...)
它根据type_spec字符串中指定的标识符,来处理输入参数,而这个参数符有很多种(具体含义可以参看: readme.parameter_parsing_api):
a a b c d f h h l l o o p p r s s z * + | / !
根据不同的组合来表示我们的php函数要接受的参数类型,比如例子中的count, 通过”z|l”表示要接受一个zval类型的参数,和一个可选的long类型的mode参数,当zend_parse_parameters在runtime的时候被调用的时候,就会需要分析这些字符,然后调用对应的逻辑,对于一些本身就很简单的函数来说,比如count,这个开销就会显得很明显。
再回头来看这个函数的特点,我们会发现,比如对于count这个例子来说,其实type_spec在编译期就是确定的常量,也就是说,其实在编译的时候,我们就应该已经知道了”a|l”应该调用那些对应的参数处理逻辑。
而事实上,当代的编译器都具备这个基本优化能力, 比如对于如下的代码:
#include <stdlib.h> #define aaa 1;int main() { int a = aaa; if (a) { abort(); } return 0;}
如果我们尝试让编译优化(-o2)它,并检查生成的汇编:
main:.lfb18: subq $8, %rsp call abort@plt
大家可以看到,if判断已经被抹掉了, 因为在编译时刻, 就能知道a是1, if一定为真。
而fast_zpp就是充分借助了这个能力而来的一种新型的参数申明方式, 比如对于z_param_zval(array)
#define z_param_zval_ex(dest, check_null, separate) \ if (separate) { \ z_param_prologue(separate); \ zend_parse_arg_zval_deref(_arg, &dest, check_null); \ } else { \ ++_i; \ zend_assert(_i <= _min_num_args || _optional==1); \ zend_assert(_i > _min_num_args || _optional==0); \ if (_optional && unexpected(_i >_num_args)) break; \ _real_arg++; \ zend_parse_arg_zval(_real_arg, &dest, check_null); \ } #define z_param_zval(dest) \ z_param_zval_ex(dest, 0, 0)
在编译时刻就能被先替换为:
zend_parse_arg_zval(((zval*)execute_data) - 1, &array, 0);
而如果我们进一步审视zend_parse_arg_zval:
static zend_always_inline void zend_parse_arg_zval(zval *arg, zval **dest, int check_null){ *dest = (check_null && (unexpected(z_type_p(arg) == is_null) || (unexpected(z_isref_p(arg)) && unexpected(z_type_p(z_refval_p(arg)) == is_null)))) ? null : arg;}
我们会发现它也是一个inline申明的函数,而参数因为是常量,那么就可以进一步被evaluate成:
zval *array = ((zval*)execute_data) - 1;
怎么样,是不是一看就知道会快很多? 没有type_spec分析,没有额外的函数调用,直接获取到参数。
刚刚说到的inline函数可以在编译时期根据常数的剪枝内联, 也是用来避免同类函数的重复代码的很好的方法,在php7中也有大量使用,有兴趣的可以参看zend_hash.c中的很多相似函数的定义。
当然,这么做也有一个问题就是, 会增大我们程序的binary size, 这个也很容易理解, 比如对于count来说,本来原来只是调用一个外部函数,一个call指令就够了,但现在就会有很多内联进来的指令。
而binary size变大以后,执行时期的cache miss就会增大,也会影响性能,所以fast_zpp我们也不是建议全部使用, 而真是针对实际应用中调用频率比较大,并且本身函数逻辑较为简单的函数来使用.
总结一下,一般来说,我们自己写的扩展函数,并不需要一定使用fast_zpp, 因为如果自身是复杂的函数逻辑的, 这点开销对比起来,其实也还好了。
最后,附上新的fast_zpp api和老的参数描述之间的对应如下:
推荐教程:《php》《php7教程》
以上就是php7 内核之 fast_zpp 详解的详细内容。
