开发内功修炼@张彦飞开发内功修炼@张彦飞

talk is cheap,
show me the code!

PHP7内存性能优化的思想精髓

大家好,我是飞哥!
前面我们讨论了内存的工作原理,也进行了一些性能相关的测试。那么今天开始我们来看几个在实践中的应用。首先我们先从PHP开始。

2015年,PHP7的发布可以说是在技术圈里引起了不小的轰动,因为它的执行效率比PHP5直接翻了一倍。PHP7在内存方面,你是否知道作者都进行了哪些优化?你是否能够深层次理解到作者优化思路的精髓?

让我们从几个核心的数据结构改进开始看起。

PHP7 zval变化

1、php5.3中的zval:

typedef unsigned int zend_object_handle;
typedef struct _zend_object_value {
    zend_object_handle handle;
    zend_object_handlers *handlers;
} zend_object_value;

typedef union _zvalue_value {
    long lval;                    /* long value */
    double dval;                /* double value */
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht;                /* hash table value */
    zend_object_value obj;
} zvalue_value;

struct _zval_struct {
    /* Variable information */
    zvalue_value value;        /* value */
    zend_uint refcount__gc;
    zend_uchar type;    /* active type */
    zend_uchar is_ref__gc;
};

我们这里只讨论64位操作系统下的情况。该_zval_struct结构体中的由四个成员构成,其中zvalue_value稍微复杂一些,是一个联合体。联合体中最长的成员是一个指针加一个int,8+4=12字节。但是默认情况下,会进行内存对齐,故_zval_struct会占用16字节。 那么

_zval_struct总的字节 = value(16)+ refcount__gc(4)+ type(1)+ is_ref__gc(1)= 占用22字节。

最后再考虑下内存对齐,实际占用24字节。(如果算的有点晕话,感兴趣的同学可以写段简单的测试代码,使用sizeof查看一下)

2、PHP7.2中的zval

typedef struct _zval_struct     zval;
typedef union _zend_value {
    zend_long         lval;                /* long value */
    double            dval;                /* double value */
    zend_refcounted  *counted;
    zend_string      *str;
    zend_array       *arr;
    zend_object      *obj;
    zend_resource    *res;
    zend_reference   *ref;
    zend_ast_ref     *ast;
    zval             *zv;
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        uint32_t w1;
        uint32_t w2;
    } ww;
} zend_value;
struct _zval_struct {
    zend_value        value;            /* value */
    union {  
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    type,            
                zend_uchar    type_flags,
                zend_uchar    const_flags,
                zend_uchar    reserved)        
        } v;
        int type_info;
    } u1;
    union {  ...... } u2;
};

7.2中的_zval_struct结构体里由3个成员构成,其中zend_value看起来比较复杂,实际上只是一个8字节的联合体。 u1也是一个联合体,占用是4个字节。u2也一样。这样_zval_struct就实际占用16个字节。

PHP7 HashTable变化

1、PHP5.3里的HashTable:

typedef struct _hashtable {
        uint nTableSize;
        uint nTableMask;
        uint nNumOfElements;   //注意这里:浪费ing
        ulong nNextFreeElement;
        Bucket *pInternalPointer;       /* Used for element traversal */
        Bucket *pListHead;
        Bucket *pListTail;
        Bucket **arBuckets;
        dtor_func_t pDestructor;
        zend_bool persistent;
        unsigned char nApplyCount;
        zend_bool bApplyProtection;
} HashTable;

再5.3里HashTable就是一个大struct, 有点小复杂,我们拆开了细说,

  • uint nTableSize 4字节
  • uint nTableMask 4字节
  • uint nNumOfElements 4字节,
  • ulong nNextFreeElement 8字节 注意这前面的4个字节会被浪费掉,因为nNextFreeElement的开始地址需要对齐
  • Bucket *pInternalPointer 8字节
  • Bucket *pListHead 8字节
  • Bucket *pListTail 8字节
  • Bucket **arBuckets 8字节
  • dtor_func_t pDestructor 8字节
  • zend_bool persistent 1字节
  • unsigned char nApplyCoun 1字节
  • zend_bool bApplyProtection 1字节

最终

总字节数 = 4+4+4+4(nNextFreeElement前面这四个字节会留空)+8+8+8+8+8+8+1+1+1 = 67字节。

再加上结构体本身要对齐到8的整数倍,所以实际占用72字节。

2、PHP7.2里的HashTable:

typedef struct _zend_array HashTable;
struct _zend_array {
    zend_refcounted_h gc;
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    flags,
                zend_uchar    nApplyCount,
                zend_uchar    nIteratorsCount,
                zend_uchar    consistency)
        } v;
        uint32_t flags;
    } u;
    uint32_t          nTableMask;
    Bucket           *arData;
    uint32_t          nNumUsed;
    uint32_t          nNumOfElements;
    uint32_t          nTableSize;
    uint32_t          nInternalPointer;
    zend_long         nNextFreeElement;
    dtor_func_t       pDestructor;
};s

在7.2里HashTable

  • zend_refcounted_h gc 看起来唬人,实际就是个long,占用8字节
  • union... u 占用4字节
  • uint32_t 占用4字节
  • Bucket* 指针占用8字节
  • uint32_t nNumUsed 占用4字节
  • uint32_t nNumOfElements 占用4字节
  • uint32_t nTableSize 占用4字节
  • uint32_t nInternalPointer 占用4字节
  • zend_long nNextFreeElement 占用8字节
  • dtor_func_t pDestructor 占用8字节
总字节数 = 8+4+4+8+4+4+4+4+8+8 = 56字节

占用56字节,并且正好达到了内存对齐的状态,没有额外的浪费。

另外还有PHP源代码里经常出镜的Buckets也从72下降到了32字节,这里我就不翻源代码了。

优化思路精髓

我们看了两个核心数据结构的结构体变化,这上面的优化都是什么含义呢? 拿HashTable举例,貌似从72字节优化到了56字节,这内存节约的也不是特别多嘛,才20%多而已!

但这中间其实隐藏了两个较深层次优化思路

第一、CPU在向内存要数据的时候是以Cache Line为单位进行的,而我们说过Cache Line的大小就是64字节。回过头来看HashTable,在7.2里的56字节,只需要CPU向内存进行一次Cache Line大小的burst IO,就够了。而在5.3里的72字节,虽然只比Cache Line大了那么一丢丢,但是对不起,必须得进行两次burst IO才可以。 所以,在计算机里,72字节相对56字节实际上是翻倍的性能提升!!

第二、CPU的L1、L2、L3的容量是固定的几十K或者几十M。假设Cache的都是HashTable,那么Cache容量不变的条件下,PHP7里能Cache住的HashTable数量将会翻倍,缓存命中率提升一大截。要知道L1命中后只需要1ns多一点的耗时,而如果穿透到内存的话可能就需要40多纳秒的延时了,整整差了几十倍。

所以PHP内核的作者大牛深谙CPU与内存的工作原理,表面上看起来只是几个字节的节约,但是实际上爆发出了巨大的性能提升!!

写在最后,由于我的这些知识在公众号里文章比较分散,很多人似乎没有理解到我对知识组织的体系结构。而且图文也不像视频那样理解起来更直接。所以我在知识星球上规划了视频系列课程,包括硬件原理、内存管理、进程管理、文件系统、网络管理、Golang语言、容器原理、性能观测、性能优化九大部分大约 120 节内容,每周更新。加入方式参见我要开始搞知识星球啦如何才能高效地学习技术,我投“融汇贯通”一票

Github:https://github.com/yanfeizhang/coder-kung-fu
关注公众号:微信扫描下方二维码
qrcode2_640.png

本原创文章未经允许不得转载 | 当前页面:开发内功修炼@张彦飞 » PHP7内存性能优化的思想精髓