(文中任何描述以及阐述不正确的地方希望大家不令赐教)
关于PHP中的数组确实是太灵活了,那么数组究竟在底层的结构是怎样的?这个是比较让人好奇的。在PHP中数组的实现是通过哈希表实现的,这个在动态语言的实现中比较常见。下面主要讲解一下PHP中的哈希表结构:
上图基本反应了hashtable的一个基本结构示例。
我们知道在C语言里数组是一个基本的内存块(chunk of memory),所以使用一定要明确数组长度而动态数组几乎是不可能的,同理associative array这种形式的也是不存在的,但在PHP里面数组是一个很灵活的数据结构,当然不仅仅PHP在现代动态语言的实现中几乎都存在这种动态灵活的数据结构,比如JS, python等等,那么他们是如何实现的,这就要用到一个结构哈希表。很多动态语言的核心其实就是一张哈希表。PHP的哈希表实现有点复杂,本文就深入阐释PHP哈希表的实现。
什么叫哈希表?
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。--维基百科
从定义看哈希表最关键的两个方面
1. 通过一关键码访问(关键码的确定,哈希函数)
2. 映射到数据结构中(哈希表本身的存储结构)
3. 映射的处理(冲突或者碰撞检测和处理函数)
对于PHP的哈希我们也从上面三个方面进行分析。一般来说对于整形索引进行哈希我们很容易想到的是取模运算,比如array(1=>'a', 2=>'b', 3=>'c'),这类我们可以使用index%3来哈希,不过PHP数组的下标还有更灵活的array('a'='c', 'b'=>'d'), 此时选择什么哈希函数?答案是DJBX33A算法(DJBX33A算法,也就是time33算法,是APR默认哈希算法,php, apache, perl, bsddb也都使用time33哈希。对于33这个数,DJB注释中是说,1到256之间的所有奇数,都能达到一个可接受的哈希分布,平均分布大概是86%。而其中33,17,31,63,127,129这几个数在面对大量的哈希运算时有一个更大的优势,就是这些数字能将乘法用位运算配合加减法替换,这样运算速度会更高。gcc编译器开启优化后会自动将乘法转换为位运算。)
static inline ulong zend_inline_hash_func(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;
}
有了哈希函数之后那么哈希表本身的存储结构如何?这里需要说明两种PHP底层的数据结构HashTable 和 Bucket
typedef struct _hashtable {
uint nTableSize;
uint nTableMask;
uint nNumOfElements;
ulong nNextFreeElement;
Bucket *pInternalPointer;
Bucket *pListHead;
Bucket *pListTail;
Bucket **arBuckets;
dtor_func_t pDestructor;
zend_bool persistent;
unsigned char nApplyCount;
zend_bool bApplyProtection;
#if ZEND_DEBUG
int inconsistent;
#endif
} HashTable
上述结构体定义了PHP底层的存储结构,逐个字段做个解释:
1. nNumOfElements 是PHP数组中实际存储元素的个数,我们使用count,sizeof计算的就是获取的这个值。
2. nTableSize 顾名思义这个是整个哈希表分配的大小(在内部实现的C中分配的数组大小,PHP是动态的但到底层数组是有大小的是静态的),他的大小有一个固定的申请算法,一般是最接近并且大于当前这个数值的2的乘方,描述的可能有点模糊,举个例子来看,如果PHP数组存储32个整形数据,那么底层申请的nTableSize应该等于32个元素,如果33呢,那么取最近且大于这个数的一个数64,那么分配的大小是64个元素。这样分配的原因是为了能分配足够的内存同样又不会浪费太多的内存。基于哈希的效率考虑,太小那么势必造成哈希之后太多的碰撞查找,如果分配太大那么必然浪费太多内存,这样分配经过实践证明相对在空间和时间上可以获得一个平衡。
3. nTableMask 哈希表的掩码数值等于nTableSize-1,他的作用是什么?用来纠正通过上面DBJ算法计算的哈希值在当前nTableSize大小的哈希表中的正确的索引值。比如"foo"通过固定算法之后得出的哈希值是193491849,如果表的大小为64,很明显已经超过了最大索引值,这时候就需要运用哈希表的掩码对其进行矫正实际采用的方法就是与掩码进行位运与运算
hash | 193491849 | 0b1011100010000111001110001001
& mask | & 63 | & 0b0000000000000000000000111111
---------------------------------------------------------
= index | = 9 | = 0b0000000000000000000000001001
这样做是为了把哈希值大的一样映射到nTalbeSize空间内
typedef struct bucket {
ulong h;
uint nKeyLength;
void *pData;
void *pDataPtr;
struct bucket *pListNext;
struct bucket *pListLast;
struct bucket *pNext;
struct bucket *pLast;
const char *arKey;
} Bucket;
ZEND_API int zend_hash_exists(const HashTable *ht, const char *arKey, uint nKeyLength)
{
ulong h;
uint nIndex;
Bucket *p;
IS_CONSISTENT(ht);
h = zend_inline_hash_func(arKey, nKeyLength);
nIndex = h & ht->nTableMask;
p = ht->arBuckets[nIndex];
while (p != NULL) {
if (p->arKey == arKey ||
((p->h == h) && (p->nKeyLength == nKeyLength) && !memcmp(p->arKey, arKey, nKeyLength))) {
return 1;
}
p = p->pNext;
}
return 0;
}