http://ju.outofmemory.cn/entry/197064php
http://www.fzb.me/2015-9-16-php7-implementation-hashtable.htmlhtml
http://ju.outofmemory.cn/entry/154095git
http://www.laruence.com/2009/08/23/1065.htmlgithub
https://github.com/laruence/php7-internal/blob/master/zval.md算法
https://github.com/laruence/php7-internal/blob/master/zval.mdshell
https://github.com/laruence/php7-internal/blob/master/zval.mdapache
https://github.com/laruence/php7-internal/blob/master/zval.mdsegmentfault
http://coolshell.cn/articles/11377.html數組
http://0x1.im/blog/php/Internal-value-representation-in-PHP-7-part-1.htmlphp7
http://www.supmen.com/vnzdw8op3r.html
https://segmentfault.com/a/1190000004124429
http://www.csdn.net/article/2015-09-16/2825720
http://blog.jobbole.com/96689/
http://www.cnblogs.com/yanlingyin/archive/2011/12/07/2278961.html
http://blog.csdn.net/itianyi/article/details/8593391
http://www.laruence.com/2008/08/19/338.html
http://bestphper.com/archives/75
http://www.jianshu.com/p/9f1ae9840847
https://github.com/laruence/php7-internal/pull/28/files?diff=split#diff-0e5d955754f7e87c931aa5e4f669881eL44
php5數組的相關結構體
typedef struct _hashtable { uint nTableSize;//4 哈希表中Bucket的槽的數量,初始值爲8,每次resize時以2倍速度增加
uint nTableMask;//4 nTableSize-1 , 索引取值的優化
uint nNumOfElements;//4 哈希表中Bucket中當前存在的元素個數,count()函數會直接返回此值
ulong nNextFreeElement;//4 下一個數字索引的位置
Bucket *pInternalPointer; /* Used for element traversal 4*/ 當前遍歷的指針(foreach比for快的緣由之一) 用於元素遍歷 Bucket *pListHead;//4 存儲數組頭元素指針
Bucket *pListTail;//4 存儲數組尾元素指針
Bucket **arBuckets;//4 //指針數組,數組中每一個元素都是指針 存儲hash數組
dtor_func_t pDestructor;//4 在刪除元素時執行的回調函數,用於資源的釋放 /* persistent 指出了Bucket內存分配的方式。若是persisient爲TRUE,則使用操做系統自己的內存分配函數爲Bucket分配內存,不然使用PHP的內存分配函數。*/
zend_bool persistent;//1
unsigned char nApplyCount;//1 標記當前hash Bucket被遞歸訪問的次數(防止屢次遞歸)
zend_bool bApplyProtection;//1 標記當前hash桶容許不容許屢次訪問,不容許時,最多隻能遞歸3次
#if ZEND_DEBUG
int inconsistent;//4
#endif } HashTable; typedef struct bucket { ulong h; /* Used for numeric indexing 4字節 */ 對char *key進行hash後的值,或者是用戶指定的數字索引值/* Used for numeric indexing */
uint nKeyLength; /* The length of the key (for string keys) 4字節 字符串索引長度,若是是數字索引,則值爲0 */
void *pData; /* 4字節 實際數據的存儲地址,指向value,通常是用戶數據的副本,若是是指針數據,則指向pDataPtr*/ //這裏又是個指針,zval存放在別的地方
void *pDataPtr; /* 4字節 引用數據的存儲地址,若是是指針數據,此值會指向真正的value,同時上面pData會指向此值 */
struct bucket *pListNext; /* PHP arrays are ordered. This gives the next element in that order4字節 整個哈希表的該元素的下一個元素*/
struct bucket *pListLast; /* and this gives the previous element 4字節 整個哈希表的該元素的上一個元素*/
struct bucket *pNext; /* The next element in this (doubly) linked list 4字節 同一個槽,雙向鏈表的下一個元素的地址 */
struct bucket *pLast; /* The previous element in this (doubly) linked list 4字節 同一個槽,雙向鏈表的上一個元素的地址*/
char arKey[1]; /* Must be last element 1字節 保存當前值所對於的key字符串,這個字段只能定義在最後,實現變長結構體*/ } Bucket;
數組的初始化
ZEND_API int _zend_hash_init(HashTable *ht, uint nSize, hash_func_t pHashFunction,dtor_func_t pDestructor, zend_bool persistent ZEND_FILE_LINE_DC) { uint i = 3; //...
if (nSize >= 0x80000000) { /* prevent overflow */ ht->nTableSize = 0x80000000; } else { while ((1U << i) < nSize) { i++; } ht->nTableSize = 1 << i; } // ...
ht->nTableMask = ht->nTableSize - 1; /* Uses ecalloc() so that Bucket* == NULL */
if (persistent) { tmp = (Bucket **) calloc(ht->nTableSize, sizeof(Bucket *));//sizeof(Bucket *)大小爲4,也就是分配ht->nTableSise個指針 if (!tmp) { return FAILURE; } ht->arBuckets = tmp; } else { tmp = (Bucket **) ecalloc_rel(ht->nTableSize, sizeof(Bucket *)); if (tmp) { ht->arBuckets = tmp; } } return SUCCESS; }
數組的插入、更新
ZEND_API int _zend_hash_add_or_update(HashTable *ht, const char *arKey, uint nKeyLength, void *pData, uint nDataSize, void **pDest, int flag ZEND_FILE_LINE_DC) { //...省略變量初始化和nKeyLength <=0 的異常處理
h = zend_inline_hash_func(arKey, nKeyLength); nIndex = h & ht->nTableMask; p = ht->arBuckets[nIndex]; while (p != NULL) { if ((p->h == h) && (p->nKeyLength == nKeyLength)) { if (!memcmp(p->arKey, arKey, nKeyLength)) { // 更新操做
if (flag & HASH_ADD) { return FAILURE; } HANDLE_BLOCK_INTERRUPTIONS(); //..省略debug輸出
if (ht->pDestructor) { ht->pDestructor(p->pData); } UPDATE_DATA(ht, p, pData, nDataSize); if (pDest) { *pDest = p->pData; } HANDLE_UNBLOCK_INTERRUPTIONS(); return SUCCESS; } } p = p->pNext; } p = (Bucket *) pemalloc(sizeof(Bucket) - 1 + nKeyLength, ht->persistent); //爲Bucket分配內存,這時候的內存是不連續的,在print數組時,是在鏈表中挨個打印,內存地址是隨機的,不能使用到內存的局部性 if (!p) { return FAILURE; } memcpy(p->arKey, arKey, nKeyLength); p->nKeyLength = nKeyLength; INIT_DATA(ht, p, pData, nDataSize); p->h = h; CONNECT_TO_BUCKET_DLLIST(p, ht->arBuckets[nIndex]); //Bucket雙向鏈表操做
if (pDest) { *pDest = p->pData; } HANDLE_BLOCK_INTERRUPTIONS(); CONNECT_TO_GLOBAL_DLLIST(p, ht); // 將新的Bucket元素添加到數組的連接表的最後面
ht->arBuckets[nIndex] = p; HANDLE_UNBLOCK_INTERRUPTIONS(); ht->nNumOfElements++; ZEND_HASH_IF_FULL_DO_RESIZE(ht); /* 若是此時數組的容量滿了,則對其進行擴容。*/
return SUCCESS; }
php5 bucket中的zval只是一個指針,所以還要多分配一個指針,而php7是直接在bucket中存儲zval
ht->nTableMask的大小爲ht->nTableSize -1。 這裏使用&操做而不是使用取模,這是由於是相對來講取模操做的消耗和按位與的操做大不少。
nTableMask的做用就是將哈希值映射到槽位所能存儲的索引範圍內。 例如:某個key的索引值是21, 哈希表的大小爲8,則mask爲7,則求與時的二進制表示爲: 10101 & 111 = 101 也就是十進制的5。 由於2的整數次方-1的二進制比較特殊:後面N位的值都是1,這樣比較容易能將值進行映射, 若是是普通數字進行了二進制與以後會影響哈希值的結果。那麼哈希函數計算的值的平均分佈就可能出現影響。
因爲php7中的bucket直接存儲zval
例如
mystr = estrdup("Forty Five");
add_next_index_string(return_value, mystr);
ZEND_API int add_next_index_string(zval *arg, const char *str) /* {{{ */ { zval tmp; ZVAL_STRING(&tmp, str); return zend_hash_next_index_insert(Z_ARRVAL_P(arg), &tmp) ? SUCCESS : FAILURE; } #define ZVAL_STRINGL(z, s, l) do { \ ZVAL_NEW_STR(z, zend_string_init(s, l, 0)); \ } while (0) //zend_string_init 自己是從堆中分配的內存 static zend_always_inline zend_string *zend_string_init(const char *str, size_t len, int persistent) { zend_string *ret = zend_string_alloc(len, persistent); memcpy(ZSTR_VAL(ret), str, len); ZSTR_VAL(ret)[len] = '\0'; return ret; } static zend_always_inline zend_string *zend_string_alloc(size_t len, int persistent) { zend_string *ret = (zend_string *)pemalloc(ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent); GC_REFCOUNT(ret) = 1; /* optimized single assignment */ GC_TYPE_INFO(ret) = IS_STRING | ((persistent ? IS_STR_PERSISTENT : 0) << 8); zend_string_forget_hash_val(ret); ZSTR_LEN(ret) = len; return ret; } #define ZVAL_NEW_STR(z, s) do { \ zval *__z = (z); \ zend_string *__s = (s); \ Z_STR_P(__z) = __s; \ Z_TYPE_INFO_P(__z) = IS_STRING_EX; \ } while (0) struct _zend_string { zend_refcounted_h gc; zend_ulong h; /* hash value */ size_t len; char val[1]; };
php7數組結構體
typedef struct _HashTable { union { struct { ZEND_ENDIAN_LOHI_3( zend_uchar flags, zend_uchar nApplyCount, /* 循環遍歷保護 */ uint16_t reserve) } v; uint32_t flags; } u; uint32_t nTableSize; /* hash表的大小 HashTable的大小,始終爲2的指數(8,16,32,64...)。最小爲8,最大值根據機器不一樣而不一樣*/
uint32_t nTableMask; /* 掩碼,用於根據hash值計算存儲位置,永遠等於nTableSize-1 */ uint32_t nNumUsed; /* arData數組已經使用的數量 */ uint32_t nNumOfElements; /* hash表中元素個數 */ uint32_t nInternalPointer; /* 用於HashTable遍歷 */ zend_long nNextFreeElement; /* 下一個空閒可用位置的數字索引 */ Bucket *arData; /* 存放實際數據 */ uint32_t *arHash; /* Hash表 */ dtor_func_t pDestructor; /* 析構函數 */ } HashTable; typedef struct _Bucket { zval val; zend_ulong h; /* hash value (or numeric index) */ zend_string *key; /* string key or NULL for numerics */ } Bucket;
存儲中,最關鍵的兩個是兩個指針*arData和*arHash。其中,arData是Bucket的實際存儲位置,在HashTable初始化的時候,會分配一塊連續的能連續存放nTableSize個Bucket的內存,所以在使用時能夠將其看成數組訪問:arData[0], arData1……;arHash是一個nTableSize大小的數組,元素的key在hash以後落在0~(nTableSize-1)之間,這個數組是arData的索引,用於根據hash值迅速找到其對應的元素。
數組初始化
ZEND_API void ZEND_FASTCALL _zend_hash_init(HashTable *ht, uint32_t nSize, dtor_func_t pDestructor, zend_bool persistent ZEND_FILE_LINE_DC) { GC_REFCOUNT(ht) = 1; GC_TYPE_INFO(ht) = IS_ARRAY; ht->u.flags = (persistent ? HASH_FLAG_PERSISTENT : 0) | HASH_FLAG_APPLY_PROTECTION | HASH_FLAG_STATIC_KEYS; ht->nTableSize = zend_hash_check_size(nSize); ht->nTableMask = HT_MIN_MASK; HT_SET_DATA_ADDR(ht, &uninitialized_bucket); //在這裏已經為ht->arData分配內存,而有些文章上說在這個函數裏,ht->arData不會被初始化 ht->nNumUsed = 0; ht->nNumOfElements = 0; ht->nInternalPointer = HT_INVALID_IDX; ht->nNextFreeElement = 0; ht->pDestructor = pDestructor; }
#define HT_SET_DATA_ADDR(ht, ptr) do { \ (ht)->arData = (Bucket*)(((char*)(ptr)) + HT_HASH_SIZE((ht)->nTableMask)); \ 最少分配8個bucket } while (0)
插入,更新
static zend_always_inline zval *_zend_hash_add_or_update_i(HashTable *ht, zend_string *key, zval *pData, uint32_t flag ZEND_FILE_LINE_DC) { zend_ulong h; uint32_t nIndex; uint32_t idx; Bucket *p; IS_CONSISTENT(ht); if (UNEXPECTED(!(ht->u.flags & HASH_FLAG_INITIALIZED))) { /* 檢查hashtable是否初始化 */ CHECK_INIT(ht, 0); goto add_to_hash; } else if (ht->u.flags & HASH_FLAG_PACKED) { /* ? */ zend_hash_packed_to_hash(ht); } else if ((flag & HASH_ADD_NEW) == 0) { /* 新增 */ p = zend_hash_find_bucket(ht, key); /* 根據key查是否已經存在 */
if (p) { /* 當前的key已經存在 */ zval *data; if (flag & HASH_ADD) { /* key已經存在產生添加衝突,退出 */
return NULL; } ZEND_ASSERT(&p->val != pData); /* key存在的狀況下,值不同作更新操做 */ data = &p->val; if ((flag & HASH_UPDATE_INDIRECT) && Z_TYPE_P(data) == IS_INDIRECT) { data = Z_INDIRECT_P(data); } HANDLE_BLOCK_INTERRUPTIONS(); if (ht->pDestructor) { ht->pDestructor(data); /* 釋放掉原來的data */ } ZVAL_COPY_VALUE(data, pData); /* 將新的pData值複製給原來的data */ HANDLE_UNBLOCK_INTERRUPTIONS(); return data; } } ZEND_HASH_IF_FULL_DO_RESIZE(ht); /* If the Hash table is full, resize it */ add_to_hash: HANDLE_BLOCK_INTERRUPTIONS(); idx = ht->nNumUsed++; /* 已使用計數+1,而且用老的位置來作爲索引 */ ht->nNumOfElements++; /* 元素個數加1 */
if (ht->nInternalPointer == INVALID_IDX) { ht->nInternalPointer = idx; } p = ht->arData + idx; /* 指針加法移位 */ p->h = h = zend_string_hash_val(key); /* 計算key的hash值 */ p->key = key; zend_string_addref(key); ZVAL_COPY_VALUE(&p->val, pData); nIndex = h & ht->nTableMask; /* 與tablemask進行計算得出hash索引 */ Z_NEXT(p->val) = ht->arHash[nIndex]; /* 新的元素的hash衝突鏈表的next指向當前衝突鏈表的首部元素 */ ht->arHash[nIndex] = idx; /* 新的元素放到當前hash衝突鏈表的頭部 */ HANDLE_UNBLOCK_INTERRUPTIONS(); return &p->val; } #define Z_NEXT(zval) (zval).u2.next
php計算hash
static inline ulong zend_inline_hash_func(const char *arKey, uint nKeyLength) { register ulong hash = 5381; /* variant with the hash unrolled eight times */ for (; nKeyLength >= 8; nKeyLength -= 8) { hash = ((hash << 5) + hash) + *arKey++; hash = ((hash << 5) + hash) + *arKey++; hash = ((hash << 5) + hash) + *arKey++; hash = ((hash << 5) + hash) + *arKey++; hash = ((hash << 5) + hash) + *arKey++; hash = ((hash << 5) + hash) + *arKey++; hash = ((hash << 5) + hash) + *arKey++; hash = ((hash << 5) + hash) + *arKey++; } switch (nKeyLength) { case 7: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */ case 6: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */ case 5: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */ case 4: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */ case 3: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */ case 2: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */ case 1: hash = ((hash << 5) + hash) + *arKey++; break; case 0: break; EMPTY_SWITCH_DEFAULT_CASE() } return hash; }
相對於apache等其餘軟件使用的time33算法而言,PHP並無直接乘33,而是使用的hash << 5 + hash,這樣比乘法速度更快。從這個函數能夠看出來PHP鼓勵hash字符串的長度小於等於8位,通常也不會有人把key的長度設置的超過8位吧。說白了就是以空間換時間,哈希的字符串長度大於8位時一次for循環就執行了8次hash
hash的初始值設置成了5381, 相比在Apache中的times算法和Perl中的Hash算法(都採用初始hash爲0), 爲何是5381?
這是個神奇的數字,集素數、奇數、缺數爲一身,並且它的二進制也很獨特。在測試中,5381能夠致使哈希碰撞更少,避免雪崩。
case後面的常量表達式實際上只起語句標號做用,而不起條件判斷做用,即"只是開始執行處的入口標號". 所以,一旦與switch後面圓括號中表達式的值匹配,就今後標號處開始執行,並且執行完一個case後面的語句後,若沒遇到break語句,就自動進入 下一個case繼續執行,而不在判斷是否與之匹配,直到遇到break語句才中止執行,退出break語句.所以,若想執行一個case分以後當即跳出 switch語句,就必須在此分支的最後添加一個break語句.
HashTable的大小,始終爲2的指數(8,16,32,64...)。最小爲8,最大值根據機器不一樣而不一樣