请选择 进入手机版 | 继续访问电脑版
设为首页收藏本站

亿仁网

 找回密码
 立即注册

扫一扫,访问微社区

QQ登录

只需一步,快速开始

搜索
热搜: 活动 交友 discuz
查看: 259|回复: 0

PHP7 内核之 FAST_ZPP 详解

[复制链接]
  • TA的每日心情
    奋斗
    2019-3-14 22:24
  • 签到天数: 160 天

    [LV.7]常住居民III

    1074

    主题

    1139

    帖子

    1万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    18046
     楼主| 发表于 2020-6-9 11:35:48 | 显示全部楼层 |阅读模式

    从PHP7开始,大家可能会发现,不少函数不再使用传统的参数处理方式,而是改用了我们称之为Fast zend parameters parsing(FAST_ZPP)的新型方式, 比如在PHP7之前,count函数是这样的:



    1. PHP_FUNCTION(count)
    2. {
    3. zval *array;
    4. long mode = COUNT_NORMAL;
    5. if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z|l", &array, &mode) == FAILURE) {
    6. return;
    7. }
    8. ....
    9. }
    10. 在PHP7以后,变成了:
    11. PHP_FUNCTION(count)
    12. {
    13. zval *array;
    14. zend_long mode = COUNT_NORMAL;
    15. ZEND_PARSE_PARAMETERS_START(1, 2)
    16. Z_PARAM_ZVAL(array)
    17. Z_PARAM_OPTIONAL
    18. Z_PARAM_LONG(mode)
    19. ZEND_PARSE_PARAMETERS_END();
    20. ...
    21. }
    复制代码

    很多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”应该调用那些对应的参数处理逻辑。

    而事实上,当代的编译器都具备这个基本优化能力, 比如对于如下的代码:

    1. #include <stdlib.h>

    2. #define AAA  1;

    3. int main() {

    4. int a = AAA;

    5. if (a) {

    6. abort();

    7. }

    8. return 0;

    9. }
    复制代码

    如果我们尝试让编译优化(-o2)它,并检查生成的汇编:

    1. main:
    2. .LFB18:
    3. subq    $8, %rsp
    4. call    abort@PLT
    复制代码

    大家可以看到,if判断已经被抹掉了, 因为在编译时刻, 就能知道a是1, if一定为真。

    而FAST_ZPP就是充分借助了这个能力而来的一种新型的参数申明方式, 比如对于Z_PARAM_ZVAL(array)

    1. #define Z_PARAM_ZVAL_EX(dest, check_null, separate) \
    2. if (separate) { \
    3. Z_PARAM_PROLOGUE(separate); \
    4. zend_parse_arg_zval_deref(_arg, &dest, check_null); \
    5. } else { \
    6. ++_i; \
    7. ZEND_ASSERT(_i <= _min_num_args || _optional==1); \
    8. ZEND_ASSERT(_i >  _min_num_args || _optional==0); \
    9. if (_optional && UNEXPECTED(_i >_num_args)) break; \
    10. _real_arg++; \
    11. zend_parse_arg_zval(_real_arg, &dest, check_null); \
    12. }
    13. #define Z_PARAM_ZVAL(dest) \
    14. Z_PARAM_ZVAL_EX(dest, 0, 0)
    复制代码

    在编译时刻就能被先替换为:

    zend_parse_arg_zval(((zval*)execute_data) - 1, &array, 0);

    而如果我们进一步审视zend_parse_arg_zval:

    1. #define Z_PARAM_ZVAL_EX(dest, check_null, separate) \
    2. if (separate) { \
    3. Z_PARAM_PROLOGUE(separate); \
    4. zend_parse_arg_zval_deref(_arg, &dest, check_null); \
    5. } else { \
    6. ++_i; \
    7. ZEND_ASSERT(_i <= _min_num_args || _optional==1); \
    8. ZEND_ASSERT(_i >  _min_num_args || _optional==0); \
    9. if (_optional && UNEXPECTED(_i >_num_args)) break; \
    10. _real_arg++; \
    11. zend_parse_arg_zval(_real_arg, &dest, check_null); \
    12. }
    13. #define Z_PARAM_ZVAL(dest) \
    14. Z_PARAM_ZVAL_EX(dest, 0, 0)
    复制代码

    在编译时刻就能被先替换为:

    zend_parse_arg_zval(((zval*)execute_data) - 1, &array, 0);

    而如果我们进一步审视zend_parse_arg_zval:

    1. static zend_always_inline void zend_parse_arg_zval(zval *arg,
    2. zval **dest, int check_null)

    3. {

    4. *dest = (check_null &&

    5. (UNEXPECTED(Z_TYPE_P(arg) == IS_NULL) ||

    6. (UNEXPECTED(Z_ISREF_P(arg)) &&

    7. UNEXPECTED(Z_TYPE_P(Z_REFVAL_P(arg)) == IS_NULL)))) ? NULL : arg;

    8. }
    复制代码

    我们会发现它也是一个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, 因为如果自身是复杂的函数逻辑的, 这点开销对比起来,其实也还好了。


    造物之前,必先造人。
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|Archiver|手机版|小黑屋|亿仁网 ( 粤ICP备16098737  

    GMT+8, 2020-8-12 05:18 , Processed in 0.814302 second(s), 26 queries .

    Powered by Discuz! X3.4

    © 2001-2017 Comsenz Inc.

    快速回复 返回顶部 返回列表