设为首页 收藏本站
查看: 359|回复: 0

[经验分享] 详解PHP中Array结构HashTable

[复制链接]

尚未签到

发表于 2017-4-3 07:57:48 | 显示全部楼层 |阅读模式
  我们知道PHP中的Array在内部是以Hash的结构进行存储的。本文主要重点也是对PHP中Array的静态结构和动态结构进行分析和记录。
  这里的静态结构,是指存储PHP中Array数据时使用的数据结构,即所谓的HashTable。
  动态结构,是指程序在运行过程中,Array数据的存储状态。
  首先PHP中的hashTable的结构如下:

typedef struct bucket {
ulong h;                        /* Used for numeric indexing */
uint nKeyLength;
void *pData;
void *pDataPtr;
struct bucket *pListNext;
struct bucket *pListLast;
struct bucket *pNext;
struct bucket *pLast;
char *arKey;
} Bucket;

typedef struct _hashtable {
uint nTableSize;
uint nTableMask;
uint nNumOfElements;
ulong nNextFreeElement;
Bucket *pInternalPointer;   /* Used for element traversal */
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中的Array在内部对应一个HashTable,HashTable内部的四个Bucket类型的指针数据记录着数组实际存储的元素内容的地址。具体的内容,各字段名都可以自解释,不做多说明了。
  如果只看这几行代码,可能无法理解PHP数组实际的工作原理,接下来,我们可以手工模拟一下PHP数组中的一些最简单的操作。
  1. 从无到有
  HashTable的初始化,首先需要给一个HashTable构造一个内存空间,具体代码如下:

//hash_func_t在函数内用不到,hash函数在PHP范围内都是固定的
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;
SET_INCONSISTENT(HT_OK);
if (nSize >= 0x80000000) {
/* prevent overflow */
ht->nTableSize = 0x80000000;
} else {
while ((1U << i) < nSize) {
i++;
}
ht->nTableSize = 1 << i;
}
ht->nTableMask = 0; /* 0 means that ht->arBuckets is uninitialized */
ht->pDestructor = pDestructor;
ht->arBuckets = (Bucket**)&uninitialized_bucket;   //实际的数据存储空间还未创建
ht->pListHead = NULL;
ht->pListTail = NULL;
ht->nNumOfElements = 0;                   //表示数组内还没有一个元素,
ht->nNextFreeElement = 0;
ht->pInternalPointer = NULL;
ht->persistent = persistent;
ht->nApplyCount = 0;
ht->bApplyProtection = 1;
return SUCCESS;
}

  上述代码可以理解为,为数组构造了一个总的大门,数据都可以经由这个门进入到自己对应的内存块中。当然现在门里还没有“座位”呢。
  2. 数据插入
  对于一个一无所有的空间,怎么给它加点东西呢?这就是数据的插入,即数据是如何保存到这个HashTable中的。
  PHP的数组索引可以是数值或字符串,我们首先看字符串的索引如何存储,代码如下:

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;
}
CHECK_INIT(ht);                            //检查数组空间是否初始化
h = zend_inline_hash_func(arKey, nKeyLength); //计算字符串索引的hash值
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))) {
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
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;
}
if (IS_INTERNED(arKey)) {
p = (Bucket *) pemalloc(sizeof(Bucket), ht->persistent);
if (!p) {
return FAILURE;
}
p->arKey = (char*)arKey;
} else {
p = (Bucket *) pemalloc(sizeof(Bucket) + nKeyLength, ht->persistent);
if (!p) {
return FAILURE;
}
p->arKey = (char*)(p + 1);
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]);
if (pDest) {
*pDest = p->pData;
}
HANDLE_BLOCK_INTERRUPTIONS();
CONNECT_TO_GLOBAL_DLLIST(p, ht);
ht->arBuckets[nIndex] = p;
HANDLE_UNBLOCK_INTERRUPTIONS();
ht->nNumOfElements++;
ZEND_HASH_IF_FULL_DO_RESIZE(ht);/* If the Hash table is full, resize it */
return SUCCESS;
}
  首先,检查数组空间是否初始化,代码如下:

#define CHECK_INIT(ht) do {                                             \
if (UNEXPECTED((ht)->nTableMask == 0)) {                                \
(ht)->arBuckets = (Bucket **) pecalloc((ht)->nTableSize, sizeof(Bucket *), (ht)->persistent);   \
(ht)->nTableMask = (ht)->nTableSize - 1;                        \
}                                                                   \
} while (0)

  然后计算要插入的字符串索引的hash值,并与nTableMask做按位与,得到nindex,这个nIndex就是对应的bucket*在二维数组arBucket**中的偏移量。根据代码逻辑,如果nIndex位置不为空,则说明当前计算得到的hash值之前存在。如果连key也相同并且flag为HASH_ADD则失败,否则就是更新操作。如果是更新操作则不会对现有数组结构有任何影响,更新了对应的值之后直接退出即可。
  在需要有新元素插入到HashTable时,构造好的新元素会经过两步来链入该HashTable
  第一步代码如下:

#define CONNECT_TO_BUCKET_DLLIST(element, list_head)        \
(element)->pNext = (list_head);                         \
(element)->pLast = NULL;                                \
if ((element)->pNext) {                                 \
(element)->pNext->pLast = (element);                \
}

  在这一步中如果新元素的key的hash值之前存在过,则list_head为HashTable.arBucket[nIndex],nIndex怎么来的前面已经说过了。在这一步过后会将HashTable.arBucket[nIndex]赋值为当前的新元素,你懂得。
  如果新元素的key对应的hash之前没有存在过,则list_head就为NULL,因为HashTable.arBucket[nIndex]为NULL。你也懂得。
  第二步代码如下:

#define CONNECT_TO_GLOBAL_DLLIST(element, ht)               \
(element)->pListLast = (ht)->pListTail;                 \
(ht)->pListTail = (element);                            \
(element)->pListNext = NULL;                            \
if ((element)->pListLast != NULL) {                     \
(element)->pListLast->pListNext = (element);        \
}                                                       \
if (!(ht)->pListHead) {                                 \
(ht)->pListHead = (element);                        \
}                                                       \
if ((ht)->pInternalPointer == NULL) {                   \
(ht)->pInternalPointer = (element);                 \
}

  关于这一步会对HashTable的内容有什么样的影响,请参看下面的动态示例。相信你也懂得。
  动态示例:
  现在我们假设数组中没有任何元素,则进行插入操作。现在我们按照代码的逻辑,手动模拟一下数据插入的过程:
  1.
  插入第一个元素A,假设其key对应的hash值为1
  则插入之后,内存中的状态如下:
  HashTable.arBucket[1]=A;
  HashTable.pListHead = A
  HashTable.pListTail = A
  HashTable.pInternalPointer = A
  A.pNext = null
  A.pLast = null
  A.pListLast = null
  A.pListNext = null
  2.
  插入第二个元素B,假设其key对应的hash值为2
  则插入之后内存的状态如下:
  HashTable.arBucket[2] = B;
  HashTable.pListHead = A
  HashTable.pListTail = B
  HashTable.pInternalPointer = A       //这个只在第一次的时候设置
  A.pNext=null
  A.pLast = null
  A.pListNext = B
  A.pListLast = null
  B.pListLast = A
  B.pListNext = null
  B.pNext = null
  B.pLast = null
  3.
  插入第三个元素C,假设其key的hash值为1,和A相同
  则插入之后内存状态如下:
  HashTable.arBucket[1] = C;
  HashTable.pListHead = A
  HashTable.pListTail =C
  HashTable.pInternalPointer = A       //这个只在第一次的时候设置
  A.pNext=null
  A.pLast = C
  A.pListNext = B
  A.pListLast = null
  B.pNext = null
  B.pLast = null
  B.pListLast = A
  B.pListNext = C
  C.pNext = A
  C.pLast = null
  C.pListNext = null
  C.pListLast = B
  插入A,B,C三个值之后的内存中状态即为:
  HashTable.arBucket[1] = C;
  HashTable.pListHead = A
  HashTable.pListTail =C
  HashTable.pInternalPointer = A
  A.pNext=null
  A.pLast = C
  A.pListNext = B
  A.pListLast = null
  B.pNext = null
  B.pLast = null
  B.pListLast = A
  B.pListNext = C
  C.pNext = A
  C.pLast = null
  C.pListNext = null
  C.pListLast = B
  OK,A、B、C三个元素都已插入了,现在我们要实现两个任务:
  1.
  查找某key的元素值(value):
  如果我们要访问A元素,则提供A的key:key_a,得到对应的hash值为1
  然后找HastTable.arBucket[1]。这时HastTable.arBucket[1]其实为C不是A,但由于C的key不等于A的key,因此,要沿着pNext的指针找下去,直到NULL,而此时C.pNext就是A,即找到了key_a对应的值A。
  总之由key查找一个元素时,首先要hash,然后顺着hash后的索引位置的pNext指针一直找下去,直到NULL,如果遇到了和要查找的key相同的值,则找到,否则找不到。
  2.
  遍历数组:
  由于我们的例子中的key是字符串类型的,全部循环遍历不能用for。只能用foreach,那foreach的遍历是如何实现的呢?
  简单,根据最后的HashTable的状态,我们从HastTable.pListHead开始沿着pListNext指针顺序找下去即可了。以本文例子为例,则结果为:
  HashTable.pListHead====>A
  A.pListNext                   ====>B
  B.pListNext                   ====>C
  则最后的遍历顺序就是A,B,C,发现foreach的遍历顺序是和元素插入到数组的顺序相关的。
  如果插入的元素的key不是字符串,而是数值。则可以省去做计算hash值这一步,直接拿数值的key做为hash值使用。
  这样就不存在hash冲突的问题,这样也就不会用到每个元素的pNext、pLast两个指针了,这两个指针都只会是NULL。
  这样我们可以通过使用for循环来遍历数组了,因为不存在hash冲突。
  同样,如果我们使用foreach来遍历数组的话,遍历顺序还是元素的插入顺序,这个你当然懂得。
  ps:
  本文并未对zend中的hash结够做全面的记录,只是对本文主题涉及到的逻辑的重点代码进行了分析和演示。同时也为了能抓住重点。有些代码并未列出,如:再hash的逻辑,和索引为数值类型数据的代码等。这些可在代码文件Zend/zend_hash.c中找到详细内容。

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-359349-1-1.html 上篇帖子: 用PHP调用Lucene包来实现全文检索 下篇帖子: 【转】漫谈社区PHP 业务开发
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表