从PHP7开始,大家可能会发现,大量函数不再使用传统的参数处理方式,而是改用了我们称为快速zend参数解析(FAST_ZPP)的新型方式。
例如在PHP7之前,count函数是这样的:PHP_FUNCTION ( count ){ zval *数组; 长模式= COUNT_NORMAL ; if (zend_parse_parameters (ZEND_NUM_ARGS () TSRMLS_CC , “ z | l” , & array , & mode ) == FAILURE ) { 回报; } ....}在PHP7以后,变成了: PHP_FUNCTION ( count ){ zval *数组; zend_long 模式= COUNT_NORMAL ; ZEND_PARSE_PARAMETERS_START (1 , 2 ) Z_PARAM_ZVAL (数组) Z_PARAM_OPTIONAL Z_PARAM_LONG (模式) ZEND_PARSE_PARAMETERS_END (); ...}很多PHP扩展开发的同学可能在初次接触的时候,会觉得很陌生,不要担心,让我慢慢道来: 当时在做PHPNG的开发的时候,我们主要的发现性能提升点的一个方式就是bench各种大型实际项目,来发现占用资源比较大的部分,而最常用的benchmark对象之一是wordpress,因为它够复杂,够慢。
代表了非OO型代码类的典型应用,在实际的基准的过程中我们发现,将近有6 %的耗时被zend_parse_parameters给占用了。
实际上zend_parameters_parsing确实是一个很庞大的函数:ZEND_API int zend_parse_parameters (int num_args , const char * type_spec , ...)它根据type_spec字符串中指定的标识符,来处理输入参数,而此参数符有很多种:a a b c dfh H l L o O p P rs S z * + | /!根据不同的组合来表示我们的PHP函数要接受的参数类型,例如示例中的计数。
通过“ z | l”表示要接受一个zval类型的参数,和一个可选的长类型的模式参数。
当zend_parse_parameters在运行时的时候被调用的时候,就会需要分析这些字符,然后调用对应的逻辑,对于某些本身就很简单的函数来说,计数,这个就会一下很明显。
再回头来看这个函数的特点,我们会发现,某种针对计数这个例子来说,实际上type_spec在编译期就是确定的常数,实际上,在编译的时候,我们就应该已经知道了” a | l”应该调用那些对应的参数处理逻辑。
而事实上,当代的编译器都具备这个基本优化能力,例如关于如下的代码:#include
LFB18 : SUBQ $ 8 , % RSP 致电 abort @ PLT大家可以看到,如果判断已经被抹掉了,因为在编译时刻,就能知道a是1,如果一定为真。
而FAST_ZPP就是充分利用了这个能力而来的一种新型的参数申明方式,例如对于Z_PARAM_ZVAL(array) 的#define Z_PARAM_ZVAL_EX ( DEST , check_null ,分离) \ 如果 (单独) { \ Z_PARAM_PROLOGUE (单独); \ zend_parse_arg_zval_deref ( _arg , & dest , check_null ); \ } 其他 { \ ++ _i ; \ ZEND_ASSERT ( _i <= _min_num_args || _optional == 1 ); \ ZEND_ASSERT ( _i > _min_num_args || _optional == 0 ); \ 如果 ( _optional && UNEXPECTED ( _i > _num_args )) 中断; \ _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 , &阵列, 0 );而如果我们我们进一步审视zend_parse_arg_zval:静态 zend_always_inline 无效 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申明的函数,而参数因为是常量,那么就可以进一步被评估成:的zval *数组= ((的zval *) execute_data )- 1 ; 没有type_spec分析,没有额外的函数调用,直接获取到参数。
刚刚说到的inline函数可以在编译时期根据常数的剪枝内联,也是为了避免同类函数的重复代码的很好的方法,在PHP7中也有大量使用,有兴趣的可以查阅zend_hash.c中的很多相似函数的定义。
当然,这样做也有一个问题就是,会扩大我们程序的binary size,这个也很容易理解,比如对于计数来说,本来原来只是调用一个外部函数,一个调用指令就够了,但先就会有很多内联进来的指令。
而binary size变大以后,执行周期的cache miss将会增加,也会影响性能,所以FAST_ZPP我们也不是建议全部使用,而真是针对实际应用中调用频率比较大,并且实际上是逻辑上简单的来使用。
总结一下,一般来说,我们自己写的扩展函数,并不需要一定使用FAST_ZPP,因为如果自身是复杂的函数逻辑的,这点差异对比起来,其实也好好了。