您好,欢迎访问一九零五行业门户网

从PHP底层源码视角分析PHP 7数组的实现

php7栏目介绍php底层源码如何实现php 7数组的。
推荐:php7
php 7 数组概述
php 中的数组实际上是一个有序映射。映射是一种把 values 关联到 keys 的类型。此类型在很多方面做了优化,因此可以把它当成真正的数组,或列表(向量),散列表(是映射的一种实现),字典,集合,栈,队列以及更多可能性。由于数组元素的值也可以是另一个数组,树形结构和多维数组也是允许的。 —— php 官方文档中文版
这里主要关注两个点:
key 可以是整数,也可以是字符串。float、bool、null 类型的 key 会被转换为整数或者字符串存储,其他类型的会报错。
value 可以是任意类型。
遍历数组时,数组元素按照其 key 添加的顺序依次取出。
php 7 的数组分为 packed array 和 hash array 两种类型,在满足一定条件时可以互转。
hash array 的 key 可以是整数也可以是字符串,在 hash 冲突时使用链表(冲突链)来解决冲突问题。
packed array 的所有 key 是自然数,且依次添加的元素的 key 逐渐增大(不要求连续)。它的耗时和内存占用都比 hash 数组低。
以下仅介绍 hash array 相关的内容。
主要数据类型
下图是数组主要的数据类型:
hash 区               ardata                 data 区                                            +                                            | 指 针 指 向 data 区 的 开 始                                            v+----------+----------+----------+----------+----------+----------+----------+----------+|          |          |          |          |          |          |          |          ||ntablemask|ntablemask|  ......  |    -1    |    0     |    1     |  ......  |ntablesize||          |    +1    |          |          |          |          |          |    +1    |+---------------------------------------------------------------------------------------+|          |          |          |          |          |          |          |          || uint32_t | uint32_t |  ......  | uint32_t |  bucket  |  bucket  |  ......  |  bucket  ||          |          |          |          |          |          |          |          |+----------+----------+----------+----------+----------+----------+----------+----------+

从整体看,这是一个数组。但入口是 ardata 而不是处于最左侧的一个元素。ardata 把数组分为两部分:
左边是 hash 区,其值为 uint32_t 类型,是冲突链的第一个元素在 data 区的下标;
右边是 data 区,其值为 bucket 类型,用于存储数据及其相关信息。
由于 ardata 主要指向 data 区,因此其默认类型被配置为 bucket 指针。
在申请内存时,会把 hash 区所需的内存大小加上 data 区所需的内存大小,然后一起申请。
bucket 长什么样?
zend_types.h:

/* 数组的基本元素 */typedef struct _bucket {    zval              val;              /* 值 */    zend_ulong        h;                /* hash 值(或者整数索引) */    zend_string      *key;              /* 字符串 key(如果存储时用整数索引,则该值为 null) */} bucket;

bucket 把 key 和 value 放在一起了。
在冲突链中,bucket 是一个节点。那么此时心里会有一个疑问:怎么获取冲突链的下一个节点?
冲突链
说到链表,会很自然地想到链表元素的结构体里包含着指向下一个元素的指针 next 。例如单向链表:
typedef struct listnode {    struct listnode *next;    void *value;} listnode;

但 bucket 却不包含这个指针。
会不会在 bucket 上一层,也就是数组的结构体定义中有一个专门存放冲突链的地方?
zend_types.h:
typedef struct _zend_array hashtable;struct _zend_array {    zend_refcounted_h gc;    union {        struct {            zend_endian_lohi_4(                zend_uchar    flags,                zend_uchar    _unused,                zend_uchar    niteratorscount,                zend_uchar    _unused2)        } v;        uint32_t flags;    } u;    uint32_t    ntablemask;       // 用于把 hash 值转化为 [ntablemask, -1] 区间内的负数。根据 ntablesize 生成。    bucket     *ardata;           // 指向 data 区的指针。    uint32_t    nnumused;         // data 区最后一个有效 bucket 的下标 + 1。    uint32_t    nnumofelements;   // 存在多少个有效 bucket。删除数组元素时,会使其减一。    uint32_t    ntablesize;       // 总共有多少空间。    uint32_t    ninternalpointer;    zend_long   nnextfreeelement;    dtor_func_t pdestructor;};想错了,换个角度想想.jpg

那往 bucket 下一层看看:
zend_types.h:
typedef struct _zval_struct     zval;struct _zval_struct {    zend_value        value;    // 通用值结构。存储基础类型(double)或指针(数组、对象等等)    union {        struct {            // 省略其他定义        } v;        uint32_t type_info;        // 值的类型,例如 is_array 、is_undef    } u1;    union {        uint32_t     next;         // 指向 hash 冲突链的下一个元素    <--- 就是这里 // 省略其他定义 } u2; // u2 表示第二个 union};

惊!链表元素的 next 居然藏在 php 的通用数据类型 zval 里面。
想不到吧?.jpg
补充一点:
php hashmap 的冲突链始终是一个链表,不会像 java 的 hashmap 那样在达成一定条件时转成红黑树。这会带来一定的问题。后面再详细说明。
怎么看 hashtable ?
再看一遍结构体。
zend_types.h:
typedef struct _zend_array hashtable;struct _zend_array { zend_refcounted_h gc; union { struct { zend_endian_lohi_4( zend_uchar flags, zend_uchar _unused, zend_uchar niteratorscount, zend_uchar _unused2) } v; uint32_t flags; } u; uint32_t ntablemask; // 根据 ntablesize 生成的负数。用于把 hash 值转化为 [ntablemask, -1] 区间内的负整数,防止越界。 bucket *ardata; // 指向 data 区的指针。 uint32_t nnumused; // data 区最后一个有效 bucket 的下标 + 1。 uint32_t nnumofelements; // 存在多少个有效 bucket。删除数组元素时,会使其减一。 uint32_t ntablesize; // 总共有多少空间。 uint32_t ninternalpointer; // 内部指针。受到 reset() 、 end() 、 next() 等的影响。 zend_long nnextfreeelement; dtor_func_t pdestructor;};

有效 bucket 指的是 bucket val 的类型不为 is_undef 。也就是不为未定义的(undefined)值。无效 bucket 反之。
nnumused 、nnumofelements 、 ntablesize 的区别:
nnumused = 4nnumofelements = 3ntablesize = 8+----------+----------+-----------+----------+-----------+-----------+-----------+| | | | | | | || 0 | 1 | 2 | 3 | 4 | ...... | 7 || | | | | | | |+--------------------------------------------------------------------------------+| | | | | | | || bucket | bucket | undefined | bucket | undefined | undefined | undefined || | | bucket | | bucket | buckets | bucket |+----------+----------+-----------+----------+-----------+-----------+-----------+

数组的主要操作
php 数组主要用到的基本操作有:查找、添加、更新、删除
php 内部操作有:rehash 、扩容
其中查找是较为简单的,添加、更新、删除都包含了查找的动作,因此先看查找。
查找
由于 key 有整数和字符串这两种类型,因此查找的实现也分为两种。这里以整数 key 为例。
读源码时要注意 ht_hash_* 和 ht_data_* 开头的函数,分别代表着在 hash 区和 data 区的操作。
zend_hash.c
static zend_always_inline bucket *zend_hash_index_find_bucket(const hashtable *ht, zend_ulong h){ uint32_t nindex; uint32_t idx; bucket *p, *ardata; ardata = ht->ardata;    nindex = h | ht->ntablemask;                // 避免 hash 区越界    idx = ht_hash_ex(ardata, nindex);           // 在 hash 区取 nindex 位置的值,结果是 data 区某个 bucket 的下标    while (idx != ht_invalid_idx) {        zend_assert(idx < ht_idx_to_hash(ht->ntablesize));  // 确保 data 区没有越界        p = ht_hash_to_bucket_ex(ardata, idx);  // 用 data 区下标获取 bucket,即冲突链的第一个 bucket        if (p->h == h && !p->key) {             // 整数 key 存到 h,因此比对 h。p->key 为 null 表示 bucket 的 key 为整数 key            return p;        }        idx = z_next(p->val);                   // 没有找到的话,从当前的 bucket 获取冲突链的下一个 bucket    }    return null;                                // 链表遍历完也没找到,那就是不存在}

举个例子:
ntablesize = 8 ntablemask = -(ntablesize + ntablesize)            = (-16)            = (11111111111111111111111111110000)                   10                                              2 h          = (100000000)      = (00000101111101011110000100000000)                         10                                        2 nindex     = (h | ntablemask) = (11111111111111111111111111110000)  = (-16)                                                                   2     +  10                                                                         |     +-------------------------------------------------------------------+     |     |                  hash          ardata          data     |     |                                   +     |                                   |              +----------------------------+     v                                   v              v                            |                                                                                     |+---------+---------+----------+---------+---------+---------+----------+---------+  ||         |         |          |         |         |         |          |         |  ||   -16   |   -15   |  ......  |   -1    |    0    |    1    |  ......  |    7    |  ||         |         |          |         |         |         |          |         |  |+---------------------------------------------------------------------------------+  ||         |         |          |         |         |         |          |         |  ||    1    |    6    |  ......  |    5    | bucket0 | bucket1 |  ......  | bucket7 |  ||         |         |          |         |         |         |          |         |  |+---------+---------+----------+---------+---------+---------+----------+---------+  |                                                                                     |     +                                                 +                     ^       |     |                                                 |        next         |       |     |                                                 +---------------------+       |     |                                                                               |     +-------------------------------------------------------------------------------+

至于为什么 ntablemask = -(ntablesize + ntablesize) ,见下文的【负载因子】。
ntablemask 使得无论多大的 uint32_t ,在按位或以及转成有符号整数后,都会变成负整数,并且其值会在 [ntablemask, -1] 这个区间。
介绍完整数 key 的查找,顺便对比一下字符串 key 的查找,不同之处如下:
字符串 key 会存到 p->key 里面,而这个字符串的 hash 存到 p->h 里面。
在比较 key 的时候,整数 key 是比较两个整数是否相等,而字符串 key 会先比较 hash 是否相等,然后比较两个字符串是否相等。
添加
依然取整数 key 为例。这里不关注更新元素的部分和 packed array 的部分。
zend_hash.c:
static zend_always_inline zval *_zend_hash_index_add_or_update_i(hashtable *ht, zend_ulong h, zval *pdata, uint32_t flag){    // ... 省略代码    idx = ht->nnumused++;                       // 使用空间 + 1    nindex = h | ht->ntablemask;                // 取 hash 值对应的 hash 区的下标    p = ht->ardata + idx;                       // 获取指向新元素的指针    z_next(p->val) = ht_hash(ht, nindex);       // 新 bucket 指向 hash 区下标所指的冲突链第一个 bucket    ht_hash(ht, nindex) = ht_idx_to_hash(idx);  // hash 区下标指向新 bucket    if ((zend_long)h >= (zend_long)ht->nnextfreeelement) {        ht->nnextfreeelement = h < zend_long_max ? h + 1 : zend_long_max; }add: ht->nnumofelements++;                       // 元素个数 + 1    p->h = h;                                   // 整数 key 的下标就是 hash    p->key = null;                              // 整数 key 时,必须把 p->key 设置为 null    zval_copy_value(&p->val, pdata);            // 把要添加的值复制到新 bucket 里面    return &p->val;}

小二,上图!
nnumused       = 1 nnumofelements = 1 ntablesize     = 8 ntablemask     = (-16)            = (11111111111111111111111111110000)                       10                                              2 h              = (100000000)      = (00000101111101011110000100000000)                             10                                        2 nindex         = (h + ntablemask) = (11111111111111111111111111110000)  = (-16)                                                                       2        10                                                                             +                                                                             |     +-----------------------------------------------------------------------+     |     |                 hash          ardata          data     |     |                                  +     |                                  |    +-------------------------------------+     v                                  v    v                                     |                                                                                   |+---------+---------+---------+---------+---------+---------+---------+---------+  ||         |         |         |         |         |         |         |         |  ||   -16   |   -15   | ......  |   -1    |    0    |    1    |  ...... |    7    |  ||         |         |         |         |         |         |         |         |  |+-------------------------------------------------------------------------------+  ||         |         |         |         |         |undefined|undefined|undefined|  ||    0    |   -1    | ......  |   -1    | bucket0 | bucket1 | buckets | bucket7 |  ||         |         |         |         |         |         |         |         |  |+---------+---------+---------+---------+---------+---------+---------+---------+  |                                                                                   |     +                                                                             |     +-----------------------------------------------------------------------------+                                                        ^                                                        +                                                   可 用 的 bucket nnumused       = 2 nnumofelements = 2                       hash          ardata          data                                        +                                        |              +---------------------------+                                        v              v                           |                                                                                   |+---------+---------+---------+---------+---------+---------+---------+---------+  ||         |         |         |         |         |         |         |         |  ||   -16   |   -15   | ......  |   -1    |    0    |    1    | ......  |    7    |  ||         |         |         |         |         |         |         |         |  |+-------------------------------------------------------------------------------+  ||         |         |         |         |         |         |undefined|undefined|  ||    1    |   -1    | ......  |   -1    | bucket0 | bucket1 | buckets | bucket7 |  ||         |         |         |         |         |         |         |         |  |+---------+---------+---------+---------+---------+---------+---------+---------+  |                                                                                   |     +                                       ^   next   +                          |     |                                       +----------+                          |     |                                                                             |     +-----------------------------------------------------------------------------+

文字表述为:
获取数组 ardata 最后一个元素之后的合法位置(这个位置的内存在之前已经申请好了)。把这里的 bucket 称为 bucketa。
把 bucketa 的下标放入 bucketa 的 h 中,把要添加的元素值放入 bucketa 的 val 。
把 hash 区 (h | ntablemask) 位置指向的 data 下标存储的 bucket 称为 bucketb。
把 bucketa 的 val 的 next 指向 bucketb 。
更新hash 区 (h | ntablemask) 位置的值为 bucketa 的下标。
hash 区 -1 表示 ht_invalid_idx
在上面的添加部分,可以看到函数的定义是:
static zend_always_inline zval *_zend_hash_index_add_or_update_i(hashtable *ht, zend_ulong h, zva

它把添加和更新放在一起处理了。
实际上在添加的时候,会先使用:
zend_hash_index_find_bucket(const hashtable *ht, zend_ulong h)
来看 h 这个 key 是否存在。如果存在就执行更新,如果不在就执行添加。
更新的操作就是把 pdata 复制到找到的 bucket 里面,替换掉原先的值。
删除
删除分为三种情况:
目标 key 不存在
目标 key 存在,其指向的 bucket 处于冲突链的第一个位置
目标 key 存在,其指向的 bucket 不处于冲突链的第一个位置
目标 key 不存在,直接返回就可以了。
目标 key 存在时,包括两个主要的操作:
处理冲突链指针
释放内存
处理冲突链的指针时,分为两种情况:
在第一个位置:直接让 hash 区的值指向冲突链第二个位置的 bucket 在 data 区的下标;
不在第一个位置:同链表删除中间元素的操作。
释放内存时:
如果 key 是字符串,则尝试释放 key 的空间;
把 bucket 的 val 复制到另一个变量 data,把 bucket 的 val 的类型设置为 undefined;
尝试释放 data 所占的空间。
做删除动作的入口是:
zend_hash_del_bucket(hashtable *ht, bucket *p)

做核心操作的是:
_zend_hash_del_el_ex(hashtable *ht, uint32_t idx, bucket *p, bucket *prev)

看一看源码:
zend_hash.c:
static zend_always_inline void _zend_hash_del_el_ex(hashtable *ht, uint32_t idx, bucket *p, bucket *prev){    if (!(ht_flags(ht) & hash_flag_packed)) {        if (prev) {                                                 // 处于冲突链的中间            z_next(prev->val) = z_next(p->val);        } else {                                                    // 处于冲突链的第一个            ht_hash(ht, p->h | ht->ntablemask) = z_next(p->val);    // 让 hash 区的值指向下一个 bucket 的 data 区下标        }    }    idx = ht_hash_to_idx(idx);    ht->nnumofelements--;    // 数组元素计数器减一。此时 nnumused 保持不变。    // 如果数组内部指针指向要删除的这个 bucket ,则让其指向数组下一个有效 bucket 。    if (ht->ninternalpointer == idx || unexpected(ht_has_iterators(ht))) {        uint32_t new_idx;        new_idx = idx;        while (1) {            new_idx++;            if (new_idx >= ht->nnumused) {                break;            } else if (z_type(ht->ardata[new_idx].val) != is_undef) {                break;            }        }        if (ht->ninternalpointer == idx) {            ht->ninternalpointer = new_idx;        }        zend_hash_iterators_update(ht, idx, new_idx);    }    // 如果要删除的元素是数组的最后一个元素,则尝试从后往前多回收几个无效 bucket    if (ht->nnumused - 1 == idx) {        do {            ht->nnumused--;        } while (ht->nnumused > 0 && (unexpected(z_type(ht->ardata[ht->nnumused-1].val) == is_undef)));        ht->ninternalpointer = min(ht->ninternalpointer, ht->nnumused);    }    // key 为字符串时,释放字符串内存    if (p->key) {        zend_string_release(p->key);    }    if (ht->pdestructor) {      // 如果配置了析构函数,则调用析构函数        zval tmp;        zval_copy_value(&tmp, &p->val);        zval_undef(&p->val);        ht->pdestructor(&tmp);    } else {        zval_undef(&p->val);    // 没有析构函数,则直接将 zval 的 u1.type_info 配置为 undefind。不用释放空间,因为以后元素可以重用这个空间    }}

php 数组可拥有的最大容量
zend_types.h#if sizeof_size_t == 4# define ht_max_size 0x04000000 /* small enough to avoid overflow checks *//* 省略代码 */#elif sizeof_size_t == 8# define ht_max_size 0x80000000/* 省略代码 */#else# error unknown sizeof_size_t#endif

根据 sizeof(size_t) 的执行结果判断应该设置为 67108864 还是 2147483648 。
0x04000000 转为二进制是: 00000100000000000000000000000000 0x80000000 转为二进制是:
10000000000000000000000000000000
当 nnumused 大于等于 ntablesize 时,会触发 resize 操作,以此获取更多可使用的 bucket 。
resize 策略
resize 的定义是:
zend_hash.c:static void zend_fastcall zend_hash_do_resize(hashtable *ht)

resize 有两种策略:
rehash
双倍扩容 + rehash
之所以有不用双倍扩容的选择,是因为 php 在删除元素时,只是将对应 data 区的 bucket 的值设置为 undefined,并没有移动后面的元素。
选择的条件主要涉及 hashtable 的三个成员:
struct _zend_array {    // ...省略    uint32_t    nnumused;         // data 区最后一个有效 bucket 的下标 + 1。    uint32_t    nnumofelements;   // 存在多少个有效 bucket。删除数组元素时,会使其减一。    uint32_t    ntablesize;       // 总共有多少空间。    // ...省略}

什么情况下只需要 rehash ?
源码是:ht->nnumused > ht->nnumofelements + (ht->nnumofelements >> 5)
这里做一个转换,方便理解:
ht->nnumused - ht->nnumofelements > (ht->nnumofelements >> 5)

也就是被设置为 undefined 的 bucket 数量大于当前元素个数除以 32 向下取整的值。
例如:
当 nnumused 为 2048 , nnumofelements 为 2000 的时候,得到 2048 - 2000 < 62 ,因此执行扩容。
当 nnumused 为 2048 , nnumofelements 为 1900 的时候,得到 2048 - 1900 > 59 ,因此执行 rehash。
rehash 做以下操作:
清空 hash 区;
取两个指针,一个指向当前扫描的位置(叫做 p),一个指向迁移后的位置(叫做 q),遍历直到 p 到达 nnumused ;
p 在碰到无效 bucket 时,会继续往前走一步,不做其他事。
p 在碰到有效 bucket 时,会把 bucket 的值复制到 q 指向的 bucket 的值,并且 p 和 q 一起往前走一步。
这种做法的效率会比每次移动有效 bucket 都把后面的数据一起往前移动来得高。
重新创建冲突链;
更新内部指针,使其指向更新位置后的 bucket;
更新 nnumused,使其等于 nnumofelements 。
什么情况下双倍扩容 + rehash ?
满足只 rehash 的条件就只做 rehash,如果不满足条件并且 ntablesize 小于数组可拥有的最大容量(ht_max_size),则双倍扩容。
由于 ht_max_size 是 0x04000000 或者 0x80000000,并且 ntablesize 始终是 2 的次方,所以最后一次双倍扩容后的容量刚好是 ht_max_size 。
0x04000000 转为二进制是: 00000100000000000000000000000000 0x80000000 转为二进制是:
10000000000000000000000000000000
双倍扩容时,做以下操作:
ntablesize 变为原先的两倍;
重新申请一次 hash 区和 data 区的内存,然后把原先 data 区的数据以内存拷贝的方式复制到新的 data 区;
重新计算 ntablemask;
释放掉原先 data 区的内存;
做 rehash 。主要是为了重建 hash 区。
负载因子(load factor)
负载因子会影响 hash 碰撞的概率从而影响到耗时,也会影响 hash 区的大小来影响内存消耗。
在 php 中,用 ntablemask 和 ntablesize 的关系来体现:
负载因子 = |ntablemask / ntablesize|
负载因子为 1 的时候(php 5),ntablemask == - (ntablesize) 。
负载因子为 0.5 的时候(php 7), ntablemask == - (ntablesize + ntablesize) 。
为什么负载因子会影响时间消耗和内存消耗?
负载因子越大, ntablemask 绝对值就越小(ntablemask 本身受到 ntablesize 的影响),从而导致 hash 区变小。
hash 区一旦变小,更容易产生碰撞。也就使得冲突链更长,执行的操作会在冲突链的时间消耗变得更长。
负载因子越小,hash 区变大,使得内存消耗更多,但冲突链变短,操作耗时变小。
负载因子时间消耗内存消耗大小大小大小
所以要根据对内存和时间的要求来做调整。
php 的负载因子从 1 (php5) 降到 0.5 (php7),使得速度变快了,但同时内存消耗变大。
针对内存消耗,php 还做了个改进,增加了 packed array。
packed array
packed array 的所有 key 是自然数,且依次添加的元素的 key 逐渐增大(不要求连续)。
packed array 查询时可以直接根据下标计算目标元素的位置(相当于 c 语言的数组),因此它不需要 hash 区来加速。
不过由于在某些条件下, packed array 会转成 hash array ,所以它仍然保留 ntablemask 。只是 ntablemask 固定为最小值,当前为 -2 。
hash 区只有两个位置,其值都是 ht_invalid_idx ,也就是 -1 。
以上内容希望帮助到大家,很多phper在进阶的时候总会遇到一些问题和瓶颈,业务代码写多了没有方向感,不知道该从那里入手去提升,对此我整理了一些资料,包括但不限于:分布式架构、高可扩展、高性能、高并发、服务器性能调优、tp6,laravel,yii2,redis,swoole、swoft、kafka、mysql优化、shell脚本、docker、微服务、nginx等多个知识点高级进阶干货需要的可以免费分享给大家,需要戳这里php进阶架构师>>>视频、面试文档免费获取
本文所用源码为 php 7.4.4 的版本。
php 7 数组概述
php 中的数组实际上是一个有序映射。映射是一种把 values 关联到 keys 的类型。此类型在很多方面做了优化,因此可以把它当成真正的数组,或列表(向量),散列表(是映射的一种实现),字典,集合,栈,队列以及更多可能性。由于数组元素的值也可以是另一个数组,树形结构和多维数组也是允许的。 —— php 官方文档中文版
这里主要关注两个点:
key 可以是整数,也可以是字符串。float、bool、null 类型的 key 会被转换为整数或者字符串存储,其他类型的会报错。
value 可以是任意类型。
遍历数组时,数组元素按照其 key 添加的顺序依次取出。
php 7 的数组分为 packed array 和 hash array 两种类型,在满足一定条件时可以互转。
hash array 的 key 可以是整数也可以是字符串,在 hash 冲突时使用链表(冲突链)来解决冲突问题。
packed array 的所有 key 是自然数,且依次添加的元素的 key 逐渐增大(不要求连续)。它的耗时和内存占用都比 hash 数组低。
以下仅介绍 hash array 相关的内容。
主要数据类型
下图是数组主要的数据类型:
hash 区               ardata                 data 区                                            +                                            | 指 针 指 向 data 区 的 开 始                                            v+----------+----------+----------+----------+----------+----------+----------+----------+|          |          |          |          |          |          |          |          ||ntablemask|ntablemask|  ......  |    -1    |    0     |    1     |  ......  |ntablesize||          |    +1    |          |          |          |          |          |    +1    |+---------------------------------------------------------------------------------------+|          |          |          |          |          |          |          |          || uint32_t | uint32_t |  ......  | uint32_t |  bucket  |  bucket  |  ......  |  bucket  ||          |          |          |          |          |          |          |          |+----------+----------+----------+----------+----------+----------+----------+----------+

从整体看,这是一个数组。但入口是 ardata 而不是处于最左侧的一个元素。ardata 把数组分为两部分:
左边是 hash 区,其值为 uint32_t 类型,是冲突链的第一个元素在 data 区的下标;
右边是 data 区,其值为 bucket 类型,用于存储数据及其相关信息。
由于 ardata 主要指向 data 区,因此其默认类型被配置为 bucket 指针。
在申请内存时,会把 hash 区所需的内存大小加上 data 区所需的内存大小,然后一起申请。
bucket 长什么样?
zend_types.h:

/* 数组的基本元素 */typedef struct _bucket {    zval              val;              /* 值 */    zend_ulong        h;                /* hash 值(或者整数索引) */    zend_string      *key;              /* 字符串 key(如果存储时用整数索引,则该值为 null) */} bucket;

bucket 把 key 和 value 放在一起了。
在冲突链中,bucket 是一个节点。那么此时心里会有一个疑问:怎么获取冲突链的下一个节点?
冲突链
说到链表,会很自然地想到链表元素的结构体里包含着指向下一个元素的指针 next 。例如单向链表:
typedef struct listnode {    struct listnode *next;    void *value;} listnode;

但 bucket 却不包含这个指针。
会不会在 bucket 上一层,也就是数组的结构体定义中有一个专门存放冲突链的地方?
zend_types.h:
typedef struct _zend_array hashtable;struct _zend_array {    zend_refcounted_h gc;    union {        struct {            zend_endian_lohi_4(                zend_uchar    flags,                zend_uchar    _unused,                zend_uchar    niteratorscount,                zend_uchar    _unused2)        } v;        uint32_t flags;    } u;    uint32_t    ntablemask;       // 用于把 hash 值转化为 [ntablemask, -1] 区间内的负数。根据 ntablesize 生成。    bucket     *ardata;           // 指向 data 区的指针。    uint32_t    nnumused;         // data 区最后一个有效 bucket 的下标 + 1。    uint32_t    nnumofelements;   // 存在多少个有效 bucket。删除数组元素时,会使其减一。    uint32_t    ntablesize;       // 总共有多少空间。    uint32_t    ninternalpointer;    zend_long   nnextfreeelement;    dtor_func_t pdestructor;};想错了,换个角度想想.jpg

那往 bucket 下一层看看:
zend_types.h:
typedef struct _zval_struct     zval;struct _zval_struct {    zend_value        value;    // 通用值结构。存储基础类型(double)或指针(数组、对象等等)    union {        struct {            // 省略其他定义        } v;        uint32_t type_info;        // 值的类型,例如 is_array 、is_undef    } u1;    union {        uint32_t     next;         // 指向 hash 冲突链的下一个元素    <--- 就是这里 // 省略其他定义 } u2; // u2 表示第二个 union};

惊!链表元素的 next 居然藏在 php 的通用数据类型 zval 里面。
想不到吧?.jpg
补充一点:
php hashmap 的冲突链始终是一个链表,不会像 java 的 hashmap 那样在达成一定条件时转成红黑树。这会带来一定的问题。后面再详细说明。
怎么看 hashtable ?
再看一遍结构体。
zend_types.h:
typedef struct _zend_array hashtable;struct _zend_array { zend_refcounted_h gc; union { struct { zend_endian_lohi_4( zend_uchar flags, zend_uchar _unused, zend_uchar niteratorscount, zend_uchar _unused2) } v; uint32_t flags; } u; uint32_t ntablemask; // 根据 ntablesize 生成的负数。用于把 hash 值转化为 [ntablemask, -1] 区间内的负整数,防止越界。 bucket *ardata; // 指向 data 区的指针。 uint32_t nnumused; // data 区最后一个有效 bucket 的下标 + 1。 uint32_t nnumofelements; // 存在多少个有效 bucket。删除数组元素时,会使其减一。 uint32_t ntablesize; // 总共有多少空间。 uint32_t ninternalpointer; // 内部指针。受到 reset() 、 end() 、 next() 等的影响。 zend_long nnextfreeelement; dtor_func_t pdestructor;};

有效 bucket 指的是 bucket val 的类型不为 is_undef 。也就是不为未定义的(undefined)值。无效 bucket 反之。
nnumused 、nnumofelements 、 ntablesize 的区别:
nnumused = 4nnumofelements = 3ntablesize = 8+----------+----------+-----------+----------+-----------+-----------+-----------+| | | | | | | || 0 | 1 | 2 | 3 | 4 | ...... | 7 || | | | | | | |+--------------------------------------------------------------------------------+| | | | | | | || bucket | bucket | undefined | bucket | undefined | undefined | undefined || | | bucket | | bucket | buckets | bucket |+----------+----------+-----------+----------+-----------+-----------+-----------+

数组的主要操作
php 数组主要用到的基本操作有:查找、添加、更新、删除
php 内部操作有:rehash 、扩容
其中查找是较为简单的,添加、更新、删除都包含了查找的动作,因此先看查找。
查找
由于 key 有整数和字符串这两种类型,因此查找的实现也分为两种。这里以整数 key 为例。
读源码时要注意 ht_hash_* 和 ht_data_* 开头的函数,分别代表着在 hash 区和 data 区的操作。
zend_hash.c
static zend_always_inline bucket *zend_hash_index_find_bucket(const hashtable *ht, zend_ulong h){ uint32_t nindex; uint32_t idx; bucket *p, *ardata; ardata = ht->ardata;    nindex = h | ht->ntablemask;                // 避免 hash 区越界    idx = ht_hash_ex(ardata, nindex);           // 在 hash 区取 nindex 位置的值,结果是 data 区某个 bucket 的下标    while (idx != ht_invalid_idx) {        zend_assert(idx < ht_idx_to_hash(ht->ntablesize));  // 确保 data 区没有越界        p = ht_hash_to_bucket_ex(ardata, idx);  // 用 data 区下标获取 bucket,即冲突链的第一个 bucket        if (p->h == h && !p->key) {             // 整数 key 存到 h,因此比对 h。p->key 为 null 表示 bucket 的 key 为整数 key            return p;        }        idx = z_next(p->val);                   // 没有找到的话,从当前的 bucket 获取冲突链的下一个 bucket    }    return null;                                // 链表遍历完也没找到,那就是不存在}

举个例子:
ntablesize = 8 ntablemask = -(ntablesize + ntablesize)            = (-16)            = (11111111111111111111111111110000)                   10                                              2 h          = (100000000)      = (00000101111101011110000100000000)                         10                                        2 nindex     = (h | ntablemask) = (11111111111111111111111111110000)  = (-16)                                                                   2     +  10                                                                         |     +-------------------------------------------------------------------+     |     |                  hash          ardata          data     |     |                                   +     |                                   |              +----------------------------+     v                                   v              v                            |                                                                                     |+---------+---------+----------+---------+---------+---------+----------+---------+  ||         |         |          |         |         |         |          |         |  ||   -16   |   -15   |  ......  |   -1    |    0    |    1    |  ......  |    7    |  ||         |         |          |         |         |         |          |         |  |+---------------------------------------------------------------------------------+  ||         |         |          |         |         |         |          |         |  ||    1    |    6    |  ......  |    5    | bucket0 | bucket1 |  ......  | bucket7 |  ||         |         |          |         |         |         |          |         |  |+---------+---------+----------+---------+---------+---------+----------+---------+  |                                                                                     |     +                                                 +                     ^       |     |                                                 |        next         |       |     |                                                 +---------------------+       |     |                                                                               |     +-------------------------------------------------------------------------------+

至于为什么 ntablemask = -(ntablesize + ntablesize) ,见下文的【负载因子】。
ntablemask 使得无论多大的 uint32_t ,在按位或以及转成有符号整数后,都会变成负整数,并且其值会在 [ntablemask, -1] 这个区间。
介绍完整数 key 的查找,顺便对比一下字符串 key 的查找,不同之处如下:
字符串 key 会存到 p->key 里面,而这个字符串的 hash 存到 p->h 里面。
在比较 key 的时候,整数 key 是比较两个整数是否相等,而字符串 key 会先比较 hash 是否相等,然后比较两个字符串是否相等。
添加
依然取整数 key 为例。这里不关注更新元素的部分和 packed array 的部分。
zend_hash.c:
static zend_always_inline zval *_zend_hash_index_add_or_update_i(hashtable *ht, zend_ulong h, zval *pdata, uint32_t flag){    // ... 省略代码    idx = ht->nnumused++;                       // 使用空间 + 1    nindex = h | ht->ntablemask;                // 取 hash 值对应的 hash 区的下标    p = ht->ardata + idx;                       // 获取指向新元素的指针    z_next(p->val) = ht_hash(ht, nindex);       // 新 bucket 指向 hash 区下标所指的冲突链第一个 bucket    ht_hash(ht, nindex) = ht_idx_to_hash(idx);  // hash 区下标指向新 bucket    if ((zend_long)h >= (zend_long)ht->nnextfreeelement) {        ht->nnextfreeelement = h < zend_long_max ? h + 1 : zend_long_max; }add: ht->nnumofelements++;                       // 元素个数 + 1    p->h = h;                                   // 整数 key 的下标就是 hash    p->key = null;                              // 整数 key 时,必须把 p->key 设置为 null    zval_copy_value(&p->val, pdata);            // 把要添加的值复制到新 bucket 里面    return &p->val;}

小二,上图!
nnumused       = 1 nnumofelements = 1 ntablesize     = 8 ntablemask     = (-16)            = (11111111111111111111111111110000)                       10                                              2 h              = (100000000)      = (00000101111101011110000100000000)                             10                                        2 nindex         = (h + ntablemask) = (11111111111111111111111111110000)  = (-16)                                                                       2        10                                                                             +                                                                             |     +-----------------------------------------------------------------------+     |     |                 hash          ardata          data     |     |                                  +     |                                  |    +-------------------------------------+     v                                  v    v                                     |                                                                                   |+---------+---------+---------+---------+---------+---------+---------+---------+  ||         |         |         |         |         |         |         |         |  ||   -16   |   -15   | ......  |   -1    |    0    |    1    |  ...... |    7    |  ||         |         |         |         |         |         |         |         |  |+-------------------------------------------------------------------------------+  ||         |         |         |         |         |undefined|undefined|undefined|  ||    0    |   -1    | ......  |   -1    | bucket0 | bucket1 | buckets | bucket7 |  ||         |         |         |         |         |         |         |         |  |+---------+---------+---------+---------+---------+---------+---------+---------+  |                                                                                   |     +                                                                             |     +-----------------------------------------------------------------------------+                                                        ^                                                        +                                                   可 用 的 bucket nnumused       = 2 nnumofelements = 2                       hash          ardata          data                                        +                                        |              +---------------------------+                                        v              v                           |                                                                                   |+---------+---------+---------+---------+---------+---------+---------+---------+  ||         |         |         |         |         |         |         |         |  ||   -16   |   -15   | ......  |   -1    |    0    |    1    | ......  |    7    |  ||         |         |         |         |         |         |         |         |  |+-------------------------------------------------------------------------------+  ||         |         |         |         |         |         |undefined|undefined|  ||    1    |   -1    | ......  |   -1    | bucket0 | bucket1 | buckets | bucket7 |  ||         |         |         |         |         |         |         |         |  |+---------+---------+---------+---------+---------+---------+---------+---------+  |                                                                                   |     +                                       ^   next   +                          |     |                                       +----------+                          |     |                                                                             |     +-----------------------------------------------------------------------------+

文字表述为:
获取数组 ardata 最后一个元素之后的合法位置(这个位置的内存在之前已经申请好了)。把这里的 bucket 称为 bucketa。
把 bucketa 的下标放入 bucketa 的 h 中,把要添加的元素值放入 bucketa 的 val 。
把 hash 区 (h | ntablemask) 位置指向的 data 下标存储的 bucket 称为 bucketb。
把 bucketa 的 val 的 next 指向 bucketb 。
更新hash 区 (h | ntablemask) 位置的值为 bucketa 的下标。
hash 区 -1 表示 ht_invalid_idx
在上面的添加部分,可以看到函数的定义是:
static zend_always_inline zval *_zend_hash_index_add_or_update_i(hashtable *ht, zend_ulong h, zva

它把添加和更新放在一起处理了。
实际上在添加的时候,会先使用:
zend_hash_index_find_bucket(const hashtable *ht, zend_ulong h)
来看 h 这个 key 是否存在。如果存在就执行更新,如果不在就执行添加。
更新的操作就是把 pdata 复制到找到的 bucket 里面,替换掉原先的值。
删除
删除分为三种情况:
目标 key 不存在
目标 key 存在,其指向的 bucket 处于冲突链的第一个位置
目标 key 存在,其指向的 bucket 不处于冲突链的第一个位置
目标 key 不存在,直接返回就可以了。
目标 key 存在时,包括两个主要的操作:
处理冲突链指针
释放内存
处理冲突链的指针时,分为两种情况:
在第一个位置:直接让 hash 区的值指向冲突链第二个位置的 bucket 在 data 区的下标;
不在第一个位置:同链表删除中间元素的操作。
释放内存时:
如果 key 是字符串,则尝试释放 key 的空间;
把 bucket 的 val 复制到另一个变量 data,把 bucket 的 val 的类型设置为 undefined;
尝试释放 data 所占的空间。
做删除动作的入口是:
zend_hash_del_bucket(hashtable *ht, bucket *p)

做核心操作的是:
_zend_hash_del_el_ex(hashtable *ht, uint32_t idx, bucket *p, bucket *prev)

看一看源码:
zend_hash.c:
static zend_always_inline void _zend_hash_del_el_ex(hashtable *ht, uint32_t idx, bucket *p, bucket *prev){    if (!(ht_flags(ht) & hash_flag_packed)) {        if (prev) {                                                 // 处于冲突链的中间            z_next(prev->val) = z_next(p->val);        } else {                                                    // 处于冲突链的第一个            ht_hash(ht, p->h | ht->ntablemask) = z_next(p->val);    // 让 hash 区的值指向下一个 bucket 的 data 区下标        }    }    idx = ht_hash_to_idx(idx);    ht->nnumofelements--;    // 数组元素计数器减一。此时 nnumused 保持不变。    // 如果数组内部指针指向要删除的这个 bucket ,则让其指向数组下一个有效 bucket 。    if (ht->ninternalpointer == idx || unexpected(ht_has_iterators(ht))) {        uint32_t new_idx;        new_idx = idx;        while (1) {            new_idx++;            if (new_idx >= ht->nnumused) {                break;            } else if (z_type(ht->ardata[new_idx].val) != is_undef) {                break;            }        }        if (ht->ninternalpointer == idx) {            ht->ninternalpointer = new_idx;        }        zend_hash_iterators_update(ht, idx, new_idx);    }    // 如果要删除的元素是数组的最后一个元素,则尝试从后往前多回收几个无效 bucket    if (ht->nnumused - 1 == idx) {        do {            ht->nnumused--;        } while (ht->nnumused > 0 && (unexpected(z_type(ht->ardata[ht->nnumused-1].val) == is_undef)));        ht->ninternalpointer = min(ht->ninternalpointer, ht->nnumused);    }    // key 为字符串时,释放字符串内存    if (p->key) {        zend_string_release(p->key);    }    if (ht->pdestructor) {      // 如果配置了析构函数,则调用析构函数        zval tmp;        zval_copy_value(&tmp, &p->val);        zval_undef(&p->val);        ht->pdestructor(&tmp);    } else {        zval_undef(&p->val);    // 没有析构函数,则直接将 zval 的 u1.type_info 配置为 undefind。不用释放空间,因为以后元素可以重用这个空间    }}

php 数组可拥有的最大容量
zend_types.h#if sizeof_size_t == 4# define ht_max_size 0x04000000 /* small enough to avoid overflow checks *//* 省略代码 */#elif sizeof_size_t == 8# define ht_max_size 0x80000000/* 省略代码 */#else# error unknown sizeof_size_t#endif

根据 sizeof(size_t) 的执行结果判断应该设置为 67108864 还是 2147483648 。
0x04000000 转为二进制是: 00000100000000000000000000000000 0x80000000 转为二进制是:
10000000000000000000000000000000
当 nnumused 大于等于 ntablesize 时,会触发 resize 操作,以此获取更多可使用的 bucket 。
resize 策略
resize 的定义是:
zend_hash.c:static void zend_fastcall zend_hash_do_resize(hashtable *ht)

resize 有两种策略:
rehash
双倍扩容 + rehash
之所以有不用双倍扩容的选择,是因为 php 在删除元素时,只是将对应 data 区的 bucket 的值设置为 undefined,并没有移动后面的元素。
选择的条件主要涉及 hashtable 的三个成员:
struct _zend_array {    // ...省略    uint32_t    nnumused;         // data 区最后一个有效 bucket 的下标 + 1。    uint32_t    nnumofelements;   // 存在多少个有效 bucket。删除数组元素时,会使其减一。    uint32_t    ntablesize;       // 总共有多少空间。    // ...省略}

什么情况下只需要 rehash ?
源码是:ht->nnumused > ht->nnumofelements + (ht->nnumofelements >> 5)
这里做一个转换,方便理解:
ht->nnumused - ht->nnumofelements > (ht->nnumofelements >> 5)

也就是被设置为 undefined 的 bucket 数量大于当前元素个数除以 32 向下取整的值。
例如:
当 nnumused 为 2048 , nnumofelements 为 2000 的时候,得到 2048 - 2000 < 62 ,因此执行扩容。
当 nnumused 为 2048 , nnumofelements 为 1900 的时候,得到 2048 - 1900 > 59 ,因此执行 rehash。
rehash 做以下操作:
清空 hash 区;
取两个指针,一个指向当前扫描的位置(叫做 p),一个指向迁移后的位置(叫做 q),遍历直到 p 到达 nnumused ;
p 在碰到无效 bucket 时,会继续往前走一步,不做其他事。
p 在碰到有效 bucket 时,会把 bucket 的值复制到 q 指向的 bucket 的值,并且 p 和 q 一起往前走一步。
这种做法的效率会比每次移动有效 bucket 都把后面的数据一起往前移动来得高。
重新创建冲突链;
更新内部指针,使其指向更新位置后的 bucket;
更新 nnumused,使其等于 nnumofelements 。
什么情况下双倍扩容 + rehash ?
满足只 rehash 的条件就只做 rehash,如果不满足条件并且 ntablesize 小于数组可拥有的最大容量(ht_max_size),则双倍扩容。
由于 ht_max_size 是 0x04000000 或者 0x80000000,并且 ntablesize 始终是 2 的次方,所以最后一次双倍扩容后的容量刚好是 ht_max_size 。
0x04000000 转为二进制是: 00000100000000000000000000000000 0x80000000 转为二进制是:
10000000000000000000000000000000
双倍扩容时,做以下操作:
ntablesize 变为原先的两倍;
重新申请一次 hash 区和 data 区的内存,然后把原先 data 区的数据以内存拷贝的方式复制到新的 data 区;
重新计算 ntablemask;
释放掉原先 data 区的内存;
做 rehash 。主要是为了重建 hash 区。
负载因子(load factor)
负载因子会影响 hash 碰撞的概率从而影响到耗时,也会影响 hash 区的大小来影响内存消耗。
在 php 中,用 ntablemask 和 ntablesize 的关系来体现:
负载因子 = |ntablemask / ntablesize|
负载因子为 1 的时候(php 5),ntablemask == - (ntablesize) 。
负载因子为 0.5 的时候(php 7), ntablemask == - (ntablesize + ntablesize) 。
为什么负载因子会影响时间消耗和内存消耗?
负载因子越大, ntablemask 绝对值就越小(ntablemask 本身受到 ntablesize 的影响),从而导致 hash 区变小。
hash 区一旦变小,更容易产生碰撞。也就使得冲突链更长,执行的操作会在冲突链的时间消耗变得更长。
负载因子越小,hash 区变大,使得内存消耗更多,但冲突链变短,操作耗时变小。
负载因子时间消耗内存消耗大小大小大小
所以要根据对内存和时间的要求来做调整。
php 的负载因子从 1 (php5) 降到 0.5 (php7),使得速度变快了,但同时内存消耗变大。
针对内存消耗,php 还做了个改进,增加了 packed array。
packed array
packed array 的所有 key 是自然数,且依次添加的元素的 key 逐渐增大(不要求连续)。
packed array 查询时可以直接根据下标计算目标元素的位置(相当于 c 语言的数组),因此它不需要 hash 区来加速。
不过由于在某些条件下, packed array 会转成 hash array ,所以它仍然保留 ntablemask 。只是 ntablemask 固定为最小值,当前为 -2 。
hash 区只有两个位置,其值都是 ht_invalid_idx ,也就是 -1 。
以上就是从php底层源码视角分析php 7数组的实现的详细内容。
其它类似信息

推荐信息