> 백엔드 개발 > PHP7 > 본문

PHP 기본 소스 코드의 관점에서 PHP 7 배열 구현 분석

coldplay.xixi
풀어 주다: 2023-02-17 18:08:01
앞으로
3829명이 탐색했습니다.

php7 칼럼에서는 PHP의 기본 소스 코드가 PHP 7 배열을 구현하는 방법을 소개합니다.

PHP 기본 소스 코드의 관점에서 PHP 7 배열 구현 분석

권장: php7

PHP 7 배열 개요
PHP의 배열은 실제로 순서가 지정된 맵입니다. 맵은 값을 키에 연결하는 유형입니다. 이 유형은 여러 가지 방법으로 최적화되어 있으므로 실제 배열 또는 목록(벡터), 해시 테이블(맵 구현), 사전, 세트, ​​스택, 큐 등으로 처리할 수 있습니다. 더 많은 가능성. 배열 요소의 값은 다른 배열일 수도 있으므로 트리 구조와 다차원 배열도 허용됩니다. ——PHP 공식 문서 중국어 버전
여기서 집중해야 할 두 가지 주요 사항이 있습니다.

key는 정수 또는 문자열일 수 있습니다. Float, Bool 및 Null 유형의 키는 정수 또는 문자열로 변환되어 저장되며, 다른 유형의 경우 오류가 보고됩니다.
값은 모든 유형이 될 수 있습니다.
배열을 탐색할 때 키가 추가된 순서대로 배열 요소가 제거됩니다.
PHP 7의 배열은 압축 배열과 해시 배열의 두 가지 유형으로 나누어지며, 특정 조건이 충족되면 서로 변환될 수 있습니다.

해시 배열의 키는 정수 또는 문자열일 수 있습니다. 해시 충돌이 있는 경우 충돌을 해결하기 위해 연결된 목록(충돌 체인)이 사용됩니다.
패킹된 배열의 모든 키는 자연수이며, 순차적으로 추가되는 요소의 키는 점차 증가합니다(연속성은 필요하지 않음). 시간 소비와 메모리 사용량은 해시 배열보다 낮습니다.
다음은 해시 배열과 관련된 내용만 소개합니다.

주요 데이터 유형
다음 그림은 배열의 주요 데이터 유형을 보여줍니다.

Hash 区               arData                 Data 区

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

+----------+----------+----------+----------+----------+----------+----------+----------+
|          |          |          |          |          |          |          |          |
|nTableMask|nTableMask|  ......  |    -1    |    0     |    1     |  ......  |nTableSize|
|          |    +1    |          |          |          |          |          |    +1    |
+---------------------------------------------------------------------------------------+
|          |          |          |          |          |          |          |          |
| uint32_t | uint32_t |  ......  | uint32_t |  Bucket  |  Bucket  |  ......  |  Bucket  |
|          |          |          |          |          |          |          |          |
+----------+----------+----------+----------+----------+----------+----------+----------+
로그인 후 복사
로그인 후 복사

전체적으로 보면 배열입니다. 그러나 항목은 가장 왼쪽 요소가 아닌 arData입니다. arData는 배열을 두 부분으로 나눕니다.

왼쪽은 해시 영역이고 해당 값은 데이터 영역에 있는 충돌 체인의 첫 번째 요소의 첨자인 uint32_t 유형입니다.
오른쪽은 데이터 영역입니다. , 해당 값은 버킷 유형이며 데이터 및 이에 대한 정보를 저장하는 데 사용됩니다.
arData는 주로 Data 영역을 가리키기 때문에 기본 타입은 Bucket 포인터로 구성됩니다.

메모리 신청 시 Hash 영역에 필요한 메모리 크기와 Data 영역에 필요한 메모리 크기를 더한 후 함께 적용됩니다.

버킷은 어떻게 생겼나요?

zend_types.h:
로그인 후 복사
로그인 후 복사
/* 数组的基本元素 */
typedef struct _Bucket {
    zval              val;              /* 值 */
    zend_ulong        h;                /* hash 值(或者整数索引) */
    zend_string      *key;              /* 字符串 key(如果存储时用整数索引,则该值为 NULL) */
} Bucket;
로그인 후 복사
로그인 후 복사

Bucket은 키와 값을 함께 사용합니다.

충돌 체인에서 Bucket은 노드입니다. 그러면 내 마음 속에 질문이 생길 것입니다. 충돌 체인의 다음 노드를 얻는 방법은 무엇입니까?

충돌 체인
연결 리스트라고 하면 연결 리스트 요소의 구조에는 다음 요소를 가리키는 포인터 next가 포함되어 있다고 생각하는 것이 당연합니다. 예를 들어 단방향 연결 목록:

typedef struct listNode {
    struct listNode *next;
    void *value;
} listNode;
로그인 후 복사
로그인 후 복사

그러나 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
};
로그인 후 복사
로그인 후 복사

서프라이즈! 연결된 목록의 다음 요소는 실제로 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 값의 유형이 IS_UNDEF가 아님을 의미합니다. 즉, 정의되지 않은 값이 아닙니다. 잘못된 버킷 및 그 반대.

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, 확장

The 검색은 상대적으로 말하면 모든 포함 검색 작업을 추가, 업데이트, 삭제하므로 먼저 검색을 살펴보겠습니다.

Search
키에는 정수와 문자열의 두 가지 유형이 있으므로 검색 구현도 두 가지 유형으로 나뉩니다. 여기서는 정수 키를 예로 들어보겠습니다.

소스 코드를 읽을 때 각각 Hash 영역과 Data 영역의 작업을 나타내는 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을 바이너리로 변환하면: 0000010000000000000000000000000 0x80000000을 바이너리로 변환하면:
10000000000000000000000000000000

이중 확장 시 다음 작업을 수행합니다.

nTableSize가 원래 크기의 두 배가 됩니다.
해시 영역 및 데이터 영역에서 메모리를 다시 적용합니다. 그런 다음 원본 데이터 영역의 데이터를 메모리 복사를 통해 새 데이터 영역에 복사합니다.
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도 개선되어 패킹된 배열을 추가했습니다.

packed array

packed array의 모든 키는 자연수이며, 순차적으로 추가되는 요소의 키는 점차 증가합니다(연속성은 필요하지 않음).

패킹 배열 쿼리는 첨자(C 언어의 배열과 동일)를 기반으로 대상 요소의 위치를 ​​직접 계산할 수 있으므로 속도 향상을 위해 해시 영역이 필요하지 않습니다.

그러나 특정 조건에서는 압축 배열이 해시 배열로 변환되므로 여전히 nTableMask를 유지합니다. 단지 nTableMask가 최소값(현재 -2)으로 고정되어 있다는 것입니다.

해시 영역은 위치가 2개뿐이고 그 값은 모두 HT_INVALID_IDX, 즉 -1입니다.

위 내용이 모든 사람에게 도움이 되기를 바랍니다. 많은 PHP 사용자는 비즈니스 코드를 너무 많이 작성하고 어디서부터 개선해야할지 모릅니다. 분산 아키텍처, 높은 확장성, 고성능, 높은 동시성, 서버 성능 튜닝, TP6, laravel, YII2, Redis, Swoole, Swoft, Kafka, Mysql 최적화, 쉘 스크립트를 포함하되 이에 국한되지 않는 정보를 정리했습니다. , Docker, 마이크로서비스, Nginx 및 기타 고급 지식 포인트를 모든 사람과 무료로 공유할 수 있습니다. PHP Advanced Architect>를 받으려면 여기를 클릭해야 합니다. 사용된 소스 코드는 PHP 7.4.4입니다.

PHP 7 배열 개요 PHP의 배열은 실제로 순서가 지정된 맵입니다. 맵은 값을 키에 연결하는 유형입니다. 이 유형은 여러 가지 방법으로 최적화되어 있으므로 실제 배열 또는 목록(벡터), 해시 테이블(맵 구현), 사전, 세트, ​​스택, 큐 등으로 처리할 수 있습니다. 더 많은 가능성. 배열 요소의 값은 다른 배열일 수도 있으므로 트리 구조와 다차원 배열도 허용됩니다. ——PHP 공식 문서 중국어 버전 여기서 집중해야 할 두 가지 주요 사항이 있습니다. key는 정수 또는 문자열일 수 있습니다. Float, Bool 및 Null 유형의 키는 정수 또는 문자열로 변환되어 저장되며, 다른 유형의 경우 오류가 보고됩니다.
값은 모든 유형이 될 수 있습니다.

배열을 탐색할 때 키가 추가된 순서대로 배열 요소가 제거됩니다.

PHP 7의 배열은 압축 배열과 해시 배열의 두 가지 유형으로 나누어지며, 특정 조건이 충족되면 서로 변환될 수 있습니다.

해시 배열의 키는 정수 또는 문자열일 수 있습니다. 해시 충돌이 있는 경우 충돌을 해결하기 위해 연결된 목록(충돌 체인)이 사용됩니다. 패킹된 배열의 모든 키는 자연수이며, 순차적으로 추가되는 요소의 키는 점차 증가합니다(연속성은 필요하지 않음). 시간 소비와 메모리 사용량은 해시 배열보다 낮습니다. 다음은 해시 배열과 관련된 내용만 소개합니다.

주요 데이터 유형

다음 그림은 배열의 주요 데이터 유형을 보여줍니다.

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        = 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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:csdn.net
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
최신 이슈
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿