这是一篇关于PHP中大量使用的数据结构HashTable的介绍,通过了解HashTable的存储原理,可以搞清楚一些问题,如数组的存储及一些操作原理、count()函数是如何得出数组元素个数的、foreach比for快的原因等等。HashTable是PHP的灵魂,因为在Zend引擎中大量地使用了HashTable,如变量表,常量表,函数表等,这些都是使用HashTable保存的,另外,PHP的数组也是通过使用HashTble实现的。

首先对Hashtable做个简单介绍,Hashtable的概念实际上非常简单:字符串的键首先会被传递给一个hash函数,然后这个函数会返回一个整数(我们把它叫做hash值),而这个整数就是“通常”的数组的索引。问题是对于两个不同的字符串,调用hash函数会得到同一个hash值,而现实情况是任意字符串都可以作为键,所以键会有无数个,而数组的大小必须是提前设定好的,因为hash值必须小于数组索引的最大值,所以可以生成的hash值必须是有限的。这样用有限的hash值表示无限的键,必然会导致冲突。我们把两个不同的键的hash值是一样的情况称为冲突,任何Hashtable算法都必须提供某种机制解决这种冲突。有两种主要的处理冲突的方法。开放定址法,当冲突发生的时候,冲突的元素会被保存到一个不同的索引中;链接法,所有拥有相同的hash值的元素,它们都会被保存到一个链表中。PHP使用的就是第二种方法。

 

PHP中的HashTable的实现代码保存在Zend/zend_hash.h和Zend/zend_hash.c这两个文件中,PHP使用如下两个数据结构来实现哈希表,HashTable结构体用于保存整个哈希表需要的基本信息:

typedef struct _hashtable { 
    uint nTableSize;        // hash Bucket的大小,最小为8,以2x增长。
    uint nTableMask;        // nTableSize-1 , 索引取值的优化
    uint nNumOfElements;    // hash Bucket中当前存在的元素个数,count()函数会直接返回此值 
    ulong nNextFreeElement; // 下一个数字索引的位置
    Bucket *pInternalPointer;   // 当前遍历的指针(foreach比for快的原因之一)
    Bucket *pListHead;          // 存储数组头元素指针
    Bucket *pListTail;          // 存储数组尾元素指针
    Bucket **arBuckets;         // 存储hash数组
    dtor_func_t pDestructor;    // 在删除元素时执行的回调函数,用于资源的释放
    zend_bool persistent;       //指出了Bucket内存分配的方式。如果persisient为TRUE,则使用操作系统本身的内存分配函数为Bucket分配内存,否则使用PHP的内存分配函数。
    unsigned char nApplyCount; // 标记当前hash Bucket被递归访问的次数(防止多次递归)
    zend_bool bApplyProtection;// 标记当前hash桶允许不允许多次访问,不允许时,最多只能递归3次
#if ZEND_DEBUG
    int inconsistent;
#endif
} HashTable;

Bucket(桶)结构体用于保存具体的数据内容:
typedef struct bucket {
    ulong h;            // 对char *key进行hash后的值,或者是用户指定的数字索引值
    uint nKeyLength;    // hash关键字的长度,如果数组索引为数字,此值为0
    void *pData;        // 指向value,一般是用户数据的副本,如果是指针数据,则指向pDataPtr
    void *pDataPtr;     //如果是指针数据,此值会指向真正的value,同时上面pData会指向此值
    struct bucket *pListNext;   // 整个hash表的下一元素
    struct bucket *pListLast;   // 整个哈希表该元素的上一个元素
    struct bucket *pNext;       // 存放在同一个hash Bucket内的下一个元素
    struct bucket *pLast;       // 同一个哈希bucket的上一个元素
    // 保存当前值所对于的key字符串,这个字段只能定义在最后,实现变长结构体
    char arKey[1];              
} Bucket;

PHP底层之HashTable的实现
接下来我们通过介绍HashTable的插入/更新操作来了解它的存储原理,在PHP中不管是对数组的添加操作,还是对数组的更新操作,其最终都是调用_zend_hash_add_or_update函数完成的,下面是函数具体实现:
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)  
{  
    ulong h;  
    uint nIndex;  
    Bucket *p;  
  
    IS_CONSISTENT(ht);  
  
    if (nKeyLength <= 0) {  
#if ZEND_DEBUG  
        ZEND_PUTS("zend_hash_update: Can't put in empty key\n");  
#endif  
        return FAILURE;  
    }  
  
    // 根据键的名称和长度求出Hash值
    h = zend_inline_hash_func(arKey, nKeyLength);  
    // 将Hash值与HashTable的Mask相与,得出该键值在HashTable中的存储位置为 nIndex  
    nIndex = h & ht->nTableMask;  
  
    // 将二维数组arBucket第一维下标为 nIndex 的 Bucket 数组赋值给 p  
    p = ht->arBuckets[nIndex];  
  
    // 循环这个数组,直到最后一个元素,循环结束后p处于 Bucket 数组的最后一个位置  
    while (p != NULL) {  
        // 如果存在和本键值Hash值一样并且键长度一样的元素,就判断是否存在重复的元素  
        if ((p->h == h) && (p->nKeyLength == nKeyLength)) {  
            // 键值和要存储的键值完全一样  
            if (!memcmp(p->arKey, arKey, nKeyLength)) {  
                // 如果是新增的元素,就返回错误  
                if (flag & HASH_ADD) {  
                    return FAILURE;  
                }  
                HANDLE_BLOCK_INTERRUPTIONS();  
#if ZEND_DEBUG  
                if (p->pData == pData) {  
                    ZEND_PUTS("Fatal error in zend_hash_update: p->pData == pData\n");  
                    HANDLE_UNBLOCK_INTERRUPTIONS();  
                    return FAILURE;  
                }  
#endif  
                // 程序没有在前面 return ,说明是更新已有元素  
                // 将当前存储的数据解构  
                if (ht->pDestructor) {  
                    ht->pDestructor(p->pData);  
                }  
                // 更新数据  
                UPDATE_DATA(ht, p, pData, nDataSize);  
                // 如果需要返回更新后的数据,返回给pDest变量  
                if (pDest) {  
                    *pDest = p->pData;  
                }  
                HANDLE_UNBLOCK_INTERRUPTIONS();  
                // 如果更新成功,返回  
                return SUCCESS;  
            }  
        }  
        // 移向 Bucket 数组的下一个元素  
        p = p->pNext;  
    }  
  
    // 如果是新增,并且没有重复的元素,继续往下执行  
    // 因为Bucket数组的最后一个元素是数组,所以可以实现可变长的存储  
    // 由于 struct Bucket 在定义的时候 arKey 长度为1,所以先-1,然后再分配nKeyLength的长度  
    p = (Bucket *) pemalloc(sizeof(Bucket) - 1 + nKeyLength, ht->persistent);  
    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();  
  
    // HashTable 存储的元素加1  
    ht->nNumOfElements++;  
    ZEND_HASH_IF_FULL_DO_RESIZE(ht);        /* If the Hash table is full, resize it */  
    return SUCCESS;  
}  

整个写入或更新的操作流程如下:
1.生成hash值,通过与nTableMask执行与操作,获取在arBuckets数组中的Bucket。
2.如果Bucket中已经存在元素,则遍历整个Bucket,查找是否存在相同的key值元素,如果有并且是update调用,则执行update数据操作。
3.创建新的Bucket元素,初始化数据,并将新元素添加到当前hash值对应的Bucket链表的最前面(CONNECT_TO_BUCKET_DLLIST)。
4.将新的Bucket元素添加到数组的链接表的最后面(CONNECT_TO_GLOBAL_DLLIST)。
5.将元素个数加1,如果此时数组的容量满了,则对其进行扩容。这里的判断是依据nNumOfElements和nTableSize的大小。 如果nNumOfElements > nTableSize则会调用zend_hash_do_resize以2X的方式扩容(nTableSize << 1)。

PHP底层之HashTable的实现

后言:本文中关于hashtable的介绍以及代码均为php5的实现,目前最新的php7针对hashtable的内部实现做了较多改进,如内存的分配方式、内存占用量等,这将在后面继续介绍。
 

相关文章: