深入理解PHP7内核之FAST_ZPP

从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#定义 AAA   1 ;int  main () {    诠释一个= AAA ;    如果 ( a ) {        中止();    }    返回 0 ;}如果我们尝试让编译优化(-o2)它,并检查生成的汇编:主要:。
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,因为如果自身是复杂的函数逻辑的,这点差异对比起来,其实也好好了。
 

返回列表
上一篇:
下一篇: