page contents

深入理解PHP7内核之FAST_ZPP

以下内容希望帮助到大家

attachments-2020-03-9pfAn9u75e8197109fc84.jpg

从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(PHP7的开发项目代号)的开发的时候,我们主要的发现性能提升点的一个方式就是bench各种大型实际项目,来发现占用资源比较大的部分,而最常用的benchmark对象之一是wordpress,因为它够复杂,够慢,(它也是我们开发JIT的时候对主要基准目标:))代表了非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  <stdlib.h>
 
#定义 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中也有大量使用

当然,这样做也有一个问题就是,会扩大我们程序的binary size,这个也很容易理解,比如对于计数来说,本来原来只是调用一个外部函数,一个调用指令就够了,但先就会有很多内联进来的指令。


而binary size变大以后,执行周期的cache miss将会增加,也会影响性能,所以FAST_ZPP我们也不是建议全部使用,而真是针对实际应用中调用频率比较大,并且实际上是逻辑上简单的来使用。

总结一下,一般来说,我们自己写的扩展函数,并不需要一定使用FAST_ZPP,因为如果自身是复杂的函数逻辑的,这点差异对比起来,其实也好好了。


最后,附上新的FAST_ZPP API和老的参数描述之间的对应如下:

v2-ab625653e217af28b9bb39a6eab1db5e_720w.jpg


attachments-2020-03-arxUu6RD5e8196e26846e.jpg

  • 发表于 2020-03-30 14:53
  • 阅读 ( 666 )
  • 分类:PHP开发

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
Pack
Pack

1135 篇文章

作家榜 »

  1. 轩辕小不懂 2403 文章
  2. 小柒 1470 文章
  3. Pack 1135 文章
  4. Nen 576 文章
  5. 王昭君 209 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章