php源码学习笔记(三)

php 的数组#

今天跳过了字符串,也跳过了各种变量声明的 token 和 opcodes 的研究。 因为数组的实现真的特别有意思,研究了一下午加一晚上最后终于才悟了。激动之情溢于言表,熬夜记录一下。

首先 array 的实现是一个 哈希表 ,php 将数组的 key 哈希成一个整型 h, value 存到 bucket 中, h 和 bucket 有一个对应关系。 通过 h 就能找到 bucket。 但是不同的字符串有可能 hash 出来的 h 相同, 也就是说会出现哈希冲突。 哈希冲突也很好解决,就是把冲突的 bucket 存放在一个链表里。查找的时候先通过 h 找到链表 slot, 然后再遍历 slot 通过 key 找到正确的 bucket。

php5 的实现中, arBuckets (php7 的 arData) 那里是一个 bucket 指针。 bucket 里面有多个字段实现了一个双向链表,并且有局部链表和全局链表之分,一共是四个字段, pNext pLast pListNext pListLast。还有一个指针 pData 指向 data 的地址。

下面是 php8 的源码(php7 就已经是这样了)arData 是一个 Bucket 类型的动态数组。 但是 bucket 发生了很大的改变,bucket 只剩下了 zval 类型的 val, 还有 h 和 key。

我看到这里的时候一脸懵逼,因为没有全局链表和局部链表,就没法像之前那样先找 h , 然后冲突的再找 key 了,而且 zval 之前也看了里面简单的不能再简单,只有一个 next 支持链表的操作。原来双向链表的功能它也做不到啊。

然后仔细看书,原来 php7 使用一个数组就实现了之前全局链表和局部链表的操作,然后又因为是动态数组,在内存上是连续的所以可以通过索引快速定位到 bucket,就不需要原来耗时的遍历了。大家先看下面神奇的代码, 想一想怎么实现的。

typedef struct _Bucket {
    zval              val;
    zend_ulong        h;                /* hash value (or numeric index)   */
    zend_string      *key;              /* string key or NULL for numerics */
} Bucket;

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; // 这个数是一个负数 
    Bucket           *arData; 
    uint32_t          nNumUsed; // bucket数量
    uint32_t          nNumOfElements; // 有效bucket数量
    uint32_t          nTableSize; // table size 
    uint32_t          nInternalPointer;
    zend_long         nNextFreeElement;
    dtor_func_t       pDestructor;
};

php7 在 arData 做了一个很神奇的操作。 正常动态数组,需要分配的大小是 单个数组项的大小乘以成员数量, 比如假设 php7 的 bucket 是 24 字节,那么我要声明一个包含 8 个项的数组应该是 192 个字节大小。现在 php7 的 arData 会占用 192 + 4 * slot 数量个内存大小,比如数字 key 不存在哈希冲突的情况下就是 192 + 4 * 8 个 内存大小。多出来的 32 用来创建索引。通过索引能直接指向到某个 bucket 地址。

书上用下面这个图说明这个内存结构。说实话其实对我产生了误导,这里它把负数索引放到了内存头部,其实 c 负数索引代表数组从后往前数的项。 所以实际上索引数组应该在内存空间的尾部。
php源码学习笔记(三)

它的实现逻辑其实就是通过转换 arData 的指针类型,使得操作数组时单个数组项占用的空间发生了变化。 比如我用 Bucket 类型的指针拿 占用了 192 + 32 内存空间的动态数组,那么 它就是有 (192 + 32)/ 24 个元素的数组。 用 unit_t 类型的指针拿 那么就是 (192 + 32)/4 个元素的数组。 因为内存总的大小是设计好的。 所以两种类型的数组就可以存放在一个动态数组空间里互不影响。负数索引是通过与 nTableMask 按位或实现的。

真的太帅了,大佬们真的太强了。

下面是我研究的时候写的 demo.

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

typedef struct _Bucket {
    char            val;
    unsigned long   h; 
    char            *key; 
} Bucket;

typedef struct _zend_array HashTable;

struct _zend_array {
    uint32_t          nTableMask;
    Bucket           *arData;
    uint32_t          nTableSize;
};

int main(){
    // 首先创建结构体, 初始化参数
    struct _zend_array arr;
    arr.nTableSize = 8;
    arr.nTableMask = -8;

    // 分配数组空间
    unsigned long size = -1 * arr.nTableMask * sizeof(uint32_t) +  arr.nTableSize * sizeof(Bucket);
    printf("%ld, %ld, %ld \r\n", size, -1 * arr.nTableMask * sizeof(uint32_t), arr.nTableSize * sizeof(Bucket));
    Bucket *dynamicArray = (Bucket *)malloc(size);
    // 
    arr.arData = dynamicArray;

    // 查找第一个索引
    printf("%p\r\n", &((uint32_t *)(arr.arData))[-1]);
    // 查找第一个bucket 
    printf("%p\r\n", &((Bucket *)(arr.arData))[0]);
    free(dynamicArray);
    return 0;
}
224, 32, 192
0x562912b126ac
0x562912b126b0
本作品采用《CC 协议》,转载必须注明作者和本文链接
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。