ホームページ > バックエンド開発 > PHP7 > PHP の基礎となるソース コードの観点から見た PHP 7 配列の実装の分析

PHP の基礎となるソース コードの観点から見た PHP 7 配列の実装の分析

coldplay.xixi
リリース: 2023-02-17 18:08:01
転載
3866 人が閲覧しました

#php7 列では、PHP の基礎となるソース コードが PHP 7 配列を実装する方法を紹介します。

PHP の基礎となるソース コードの観点から見た PHP 7 配列の実装の分析

#推奨事項:

php7##PHP 7 配列の概要

PHP配列 in は実際には順序付けられたマップです。マップはキーに値を関連付けるタイプです。この型はさまざまな方法で最適化されているため、実際の配列、リスト (ベクトル)、ハッシュ テーブル (マップの実装)、ディクショナリ、セット、スタック、キュー、その他多くの可能性として扱うことができます。配列要素の値は別の配列にすることもできるため、ツリー構造や多次元配列も許可されます。 ——PHP 公式ドキュメント中国語版 ここで注目すべき主な点は 2 つあります:
key は整数または文字列です。 Float、Bool、および Null 型のキーは、保存するために整数または文字列に変換され、他の型についてはエラーが報告されます。
値は任意の型にすることができます。

配列を走査するとき、配列要素はキーが追加された順序で取り出されます。

PHP 7 の配列はパック配列とハッシュ配列の 2 種類に分かれており、特定の条件が満たされた場合に相互に変換できます。

ハッシュ配列のキーは整数または文字列にすることができます。ハッシュの競合が発生した場合、リンクされたリスト (競合チェーン) を使用して競合を解決します。
パック配列のキーはすべて自然数であり、順番に追加される要素のキーは徐々に増加します(連続性は必要ありません)。消費時間とメモリ使用量はハッシュ配列よりも少なくなります。

以下では、ハッシュ配列に関連する内容のみを紹介します。


主なデータ型
次の図は、配列の主なデータ型を示しています。

Hash 区               arData                 Data 区

                                            +
                                            | 指 针 指 向 Data 区 的 开 始
                                            v

+----------+----------+----------+----------+----------+----------+----------+----------+
|          |          |          |          |          |          |          |          |
|nTableMask|nTableMask|  ......  |    -1    |    0     |    1     |  ......  |nTableSize|
|          |    +1    |          |          |          |          |          |    +1    |
+---------------------------------------------------------------------------------------+
|          |          |          |          |          |          |          |          |
| uint32_t | uint32_t |  ......  | uint32_t |  Bucket  |  Bucket  |  ......  |  Bucket  |
|          |          |          |          |          |          |          |          |
+----------+----------+----------+----------+----------+----------+----------+----------+
ログイン後にコピー
ログイン後にコピー

全体を見ると、これは配列です。ただし、エントリは左端の要素ではなく arData です。 arData は配列を 2 つの部分に分割します:

左側はハッシュ領域、その値は uint32_t 型で、データ領域の競合チェーンの最初の要素の添字です。

右側は側はデータ領域であり、その値はバケット型であり、データと関連情報を格納するために使用されます。

arData は主にデータ領域を指すため、そのデフォルトのタイプはバケット ポインターとして構成されます。


メモリ申請時は、Data領域に必要なメモリサイズに、Hash領域に必要なメモリサイズを加算して申請します。

バケットはどのようなものですか?

zend_types.h:
ログイン後にコピー
ログイン後にコピー
/* 数组的基本元素 */
typedef struct _Bucket {
    zval              val;              /* 值 */
    zend_ulong        h;                /* hash 值(或者整数索引) */
    zend_string      *key;              /* 字符串 key(如果存储时用整数索引,则该值为 NULL) */
} Bucket;
ログイン後にコピー
ログイン後にコピー
バケットはキーと値をまとめます。

競合チェーンでは、バケットはノードです。次に、私の頭の中に次の疑問が浮かびます: 競合チェーンの次のノードを取得するにはどうすればよいでしょうか?

競合チェーン

リンク リストについて言えば、リンク リスト要素の構造には次の要素を指すポインター next が含まれていると考えるのが自然です。たとえば、一方向リンク リスト:

typedef struct listNode {
    struct listNode *next;
    void *value;
} listNode;
ログイン後にコピー
ログイン後にコピー

ただし、バケットにはこのポインタが含まれません。

競合チェーンを格納するための特別な場所は、Bucket の 1 レベル上、つまり配列の構造定義内にありますか?

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
ログイン後にコピー
ログイン後にコピー
次に、バケットの次のレベルに移動して見てみましょう:

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
};
ログイン後にコピー
ログイン後にコピー

なんと!リンクされたリストの次の要素は、実際には PHP の一般的なデータ型 zval に隠されています。

信じられないですか? .jpg

追加点:

PHP HashMap の競合チェーンは常にリンク リストであり、JAVA の HashMap のように特定の条件が満たされた場合に赤黒ツリーに変換されません。これにより、特定の問題が発生します。詳細は後ほど。

ハッシュテーブルを確認するにはどうすればよいですか?
構造をもう一度見てください。

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;
};
ログイン後にコピー
ログイン後にコピー

有効なバケットは、タイプが IS_UNDEF ではないバケット val を参照します。つまり、不定値ではありません。無効なバケット、またはその逆。

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 の内部操作には、再ハッシュ、展開などがあります。

このうち、検索は比較的単純ですが、追加、更新、削除にはすべて検索アクションが含まれますので、最初に検索から見てみましょう。

Search

keyには整数と文字列の2種類があるため、検索の実装も2種類に分かれます。ここでは例として整数キーを取り上げます。

ソースコードを読むときは、HT_HASH_* と HT_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 がどれほど大きくても、ビット単位の OR を実行して符号付き整数に変換すると、負の整数になり、その値は [nTableMask, -1] の範囲になります。

完全な数字キーの検索を紹介します。ちなみに、文字列キーの検索と比較してください。違いは次のとおりです:

字符串 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 をバイナリに変換すると:
100000000000000000000000000000000

拡張容量を 2 倍にするには、次の操作を実行します:

nTableサイズが2倍になります元の;
Hash 領域と Data 領域のメモリを再申請し、元の Data 領域のデータをメモリコピーの形式で新しい Data 領域にコピーします;
nTableMask を再計算します;
元のデータ領域のメモリを解放します;
は再ハッシュします。主にハッシュ領域の再構築を行います。

負荷係数

負荷係数は、ハッシュ衝突の確率、ひいては時間の消費に影響します。また、ハッシュ領域のサイズにも影響し、メモリの消費。

PHP では、nTableMask と nTableSize の関係を反映するために使用されます:

負荷係数 = |nTableMask / nTableSize|

負荷係数が 1 の場合 (PHP 5) 、nTableMask == - (nTableSize) 。
負荷率が 0.5 (PHP 7) の場合、nTableMask == - (nTableSize nTableSize)。

負荷率が時間消費量とメモリ消費量に影響を与えるのはなぜですか?

負荷率が大きいほど、nTableMask の絶対値が小さくなり (nTableMask 自体は nTableSize の影響を受ける)、結果としてハッシュ領域が小さくなります。

ハッシュ領域が小さくなると、衝突が発生しやすくなります。これにより、競合チェーンが長くなり、競合チェーン内で実行される操作にかかる時間が長くなります。

負荷率が小さいほどハッシュ領域が大きくなりメモリ消費量は多くなりますが、競合連鎖が短くなり動作時間が短くなります。

負荷係数時間消費メモリ消費サイズサイズサイズ

したがって、メモリと時間の要件に応じて調整する必要があります。

PHP の負荷率が 1 (PHP5) から 0.5 (PHP7) に減少し、速度が向上しますが、同時にメモリ消費量が大きくなります。

メモリ消費量に関しては、PHP も改善され、パック配列が追加されました。

パック配列

パック配列のキーはすべて自然数であり、順番に追加される要素のキーは徐々に増加します(連続である必要はありません)。

パック配列クエリは添字(C言語の配列に相当)に基づいて対象要素の位置を直接計算できるため、ハッシュ領域が不要で高速化できます。

ただし、特定の条件下ではパック配列がハッシュ配列に変換されるため、nTableMask は保持されます。 nTableMask が最小値 (現在は -2 ) に固定されているだけです。

ハッシュ領域には 2 つの位置しかなく、その値は両方とも HT_INVALID_IDX、つまり -1 です。

上記の内容が皆様のお役に立てれば幸いです。多くの PHPer は、作業を進めていくと常に何らかの問題やボトルネックに遭遇します。彼らはビジネス コードを書きすぎて、方向性がわかりません。どこに行くのかわかりません。これに関するいくつかの情報をまとめました。分散アーキテクチャ、高スケーラビリティ、高パフォーマンス、高同時実行性、サーバー パフォーマンス チューニング、TP6、laravel、YII2、Redis、Swoole、Swoft、Kafka、Mysql などです。最適化、シェル スクリプト、Docker、マイクロサービス、Nginx、その他の高度な知識ポイントは、必要に応じて無料で誰とでも共有できます。PHP Advanced Architect>>>ビデオとインタビュー ドキュメントを無料で入手するには、ここをクリックする必要があります

この記事で使用されているソース コードはバージョン PHP 7.4.4 です。

PHP 7 配列の概要
PHP の配列は、実際には順序付けされたマップです。マップはキーに値を関連付けるタイプです。この型はさまざまな方法で最適化されているため、実際の配列、リスト (ベクトル)、ハッシュ テーブル (マップの実装)、ディクショナリ、セット、スタック、キュー、その他多くの可能性として扱うことができます。配列要素の値は別の配列にすることもできるため、ツリー構造や多次元配列も許可されます。 ——PHP 公式ドキュメント中国語版
ここで注目すべき主な点は 2 つあります:

key は整数または文字列です。 Float、Bool、および Null 型のキーは、保存するために整数または文字列に変換され、他の型についてはエラーが報告されます。
値は任意の型にすることができます。
配列を走査するとき、配列要素はキーが追加された順序で取り出されます。
PHP 7 の配列はパック配列とハッシュ配列の 2 種類に分かれており、特定の条件が満たされた場合に相互に変換できます。

ハッシュ配列のキーは整数または文字列にすることができます。ハッシュの競合が発生した場合、リンクされたリスト (競合チェーン) を使用して競合を解決します。
パック配列のキーはすべて自然数であり、順番に追加される要素のキーは徐々に増加します(連続性は必要ありません)。消費時間とメモリ使用量はハッシュ配列よりも少なくなります。
以下では、ハッシュ配列に関連する内容のみを紹介します。

主なデータ型
次の図は、配列の主なデータ型を示しています。

Hash 区               arData                 Data 区

                                            +
                                            | 指 针 指 向 Data 区 的 开 始
                                            v

+----------+----------+----------+----------+----------+----------+----------+----------+
|          |          |          |          |          |          |          |          |
|nTableMask|nTableMask|  ......  |    -1    |    0     |    1     |  ......  |nTableSize|
|          |    +1    |          |          |          |          |          |    +1    |
+---------------------------------------------------------------------------------------+
|          |          |          |          |          |          |          |          |
| uint32_t | uint32_t |  ......  | uint32_t |  Bucket  |  Bucket  |  ......  |  Bucket  |
|          |          |          |          |          |          |          |          |
+----------+----------+----------+----------+----------+----------+----------+----------+
ログイン後にコピー
ログイン後にコピー

全体を見ると、これは配列です。ただし、エントリは左端の要素ではなく arData です。 arData は配列を 2 つの部分に分割します:

左边是 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        = 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;
}
ログイン後にコピー
ログイン後にコピー

小二,上图!

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 配列の実装の分析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:csdn.net
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート