nNumUsed 、nNumOfElements 、 nTableSize 的区别:
nNumUsed = 4 nNumOfElements = 3 nTableSize = 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; }