您的位置:首页 > 编程语言 > PHP开发

PHP内核分析(五)--(哈希表(HashTable))

2015-03-17 14:17 363 查看
PHP中使用最为频繁的数据类型非字符串和数组莫属,PHP比较容易上手也得益于非常灵活的数组类型。 在开始详细介绍这些数据类型之前有必要介绍一下哈希表(HashTable)。 哈希表是PHP实现中尤为关键的数据结构。

哈希表在实践中使用的非常广泛,例如编译器通常会维护的一个符号表来保存标记,很多高级语言中也显式的支持哈希表。 哈希表通常提供查找(Search),插入(Insert),删除(Delete)等操作,这些操作在最坏的情况下和链表的性能一样为O(n)。 不过通常并不会这么坏,合理设计的哈希算法能有效的避免这类情况,通常哈希表的这些操作时间复杂度为O(1)。 这也是它被钟爱的原因。

哈希表是一种通过哈希函数,将特定的键映射到特定值的一种数据结构,它维护键和值之间一一对应关系。

键(key):用于操作数据的标示,例如PHP数组中的索引,或者字符串键等等。

槽(slot/bucket):哈希表中用于保存数据的一个单元,也就是数据真正存放的容器。

哈希函数(hash function):将key映射(map)到数据应该存放的slot所在位置的函数。

哈希冲突(hash collision):哈希函数将两个不同的key映射到同一个索引的情况。

哈希表可以理解为数组的扩展或者关联数组,数组使用数字下标来寻址,如果关键字(key)的范围较小且是数字的话, 我们可以直接使用数组来完成哈希表,而如果关键字范围太大,如果直接使用数组我们需要为所有可能的key申请空间。 很多情况下这是不现实的。即使空间足够,空间利用率也会很低,这并不理想。同时键也可能并不是数字, 在PHP中尤为如此,所以人们使用一种映射函数(哈希函数)来将key映射到特定的域中:h(key) -> index

通过合理设计的哈希函数,我们就能将key映射到合适的范围,因为我们的key空间可以很大(例如字符串key), 在映射到一个较小的空间中时可能会出现两个不同的key映射被到同一个index上的情况, 这就是我们所说的出现了冲突。 目前解决hash冲突的方法主要有两种:链接法和开放寻址法。

链接法

链接法通过使用一个链表来保存slot值的方式来解决冲突,也就是当不同的key映射到一个槽中的时候使用链表来保存这些值。 所以使用链接法是在最坏的情况下,也就是所有的key都映射到同一个槽中了,这样哈希表就退化成了一个链表, 这样的话操作链表的时间复杂度则成了O(n),这样哈希表的性能优势就没有了, 所以选择一个合适的哈希函数是最为关键的。

由于目前大部分的编程语言的哈希表实现都是开源的,大部分语言的哈希算法都是公开的算法, 虽然目前的哈希算法都能良好的将key进行比较均匀的分布,而这个假使的前提是key是随机的,正是由于算法的确定性, 这就导致了别有用心的黑客能利用已知算法的可确定性来构造一些特殊的key,让这些key都映射到 同一个槽位导致哈希表退化成单链表,导致程序的性能急剧下降,从而造成一些应用的吞吐能力急剧下降, 尤其是对于高并发的应用影响很大,通过大量类似的请求可以让服务器遭受DoS(服务拒绝攻击), 这个问题一直就存在着,只是最近才被各个语言重视起来。

哈希冲突攻击利用的哈希表最根本的弱点是:开源算法和哈希实现的确定性以及可预测性, 这样攻击者才可以利用特殊构造的key来进行攻击。要解决这个问题的方法则是让攻击者无法轻易构造 能够进行攻击的key序列。

PHP采用的是一种 治标不治本的做法: 限制用户提交数据字段数量 这样可以避免大部分的攻击,不过应用程序通常会有很多的数据输入方式,比如,SOAP,REST等等, 比如很多应用都会接受用户传入的JSON字符串,在执行json_decode()的时候也可能会遭受攻击。 所以最根本的解决方法是让哈希表的碰撞key序列无法轻易的构造,目前PHP中还没有引入不增加额外的复杂性情况下的完美解决方案。

目前PHP中HashTable的哈希冲突解决方法就是链接法。

开放寻址法

通常还有另外一种解决冲突的方法:开放寻址法。使用开放寻址法是槽本身直接存放数据, 在插入数据时如果key所映射到的索引已经有数据了,这说明发生了冲突,这是会寻找下一个槽, 如果该槽也被占用了则继续寻找下一个槽,直到寻找到没有被占用的槽,在查找时也使用同样的策略来进行。

由于开放寻址法处理冲突的时候占用的是其他槽位的空间,这可能会导致后续的key在插入的时候更加容易出现 哈希冲突,所以采用开放寻址法的哈希表的装载因子不能太高,否则容易出现性能下降。

哈希表的实现

基本的数据结构主要有两个, 一个用于保存哈希表本身,另外一个就是用于实际保存数据的单链表了,定义如下:

typedef struct _Bucket
{
char *key;
void *value;
struct _Bucket *next;
} Bucket;

typedef struct _HashTable
{
int size;
int elem_num;
Bucket** buckets;
} HashTable;


上面的定义和PHP中的实现类似,为了便于理解裁剪了大部分无关的细节,在本节中为了简化, key的数据类型为字符串,而存储的数据类型可以为任意类型。

Bucket结构体是一个单链表,这是为了解决多个key哈希冲突的问题,也就是前面所提到的的链接法。 当多个key映射到同一个index的时候将冲突的元素链接起来。

哈希函数实现:

哈希函数需要尽可能的将不同的key映射到不同的槽(slot或者bucket)中,首先我们采用一种最为简单的哈希算法实现: 将key字符串的所有字符加起来,然后以结果对哈希表的大小取模,这样索引就能落在数组索引的范围之内了。

static int hash_str(char *key)
{
int hash = 0;

char *cur = key;

while(*cur != '\0') {
hash += *cur;
++cur;
}

return hash;
}

// 使用这个宏来求得key在哈希表中的索引
#define HASH_INDEX(ht, key) (hash_str((key)) % (ht)->size)


这个哈希算法比较简单,它的效果并不好,在实际场景下不会使用这种哈希算法, 例如PHP中使用的是称为DJBX33A算法, 这里列举了Mysql,OpenSSL等开源软件使用的哈希算法, 有兴趣的读者可以前往参考。

有兴趣的读者可以运行本小节实现的哈希表实现,在输出日志中将看到很多的哈希冲突, 这是本例中使用的哈希算法过于简单造成的.

哈希表算法

#ifndef _HASH_TABLE_H_
#define _HASH_TABLE_H_ 1

#define HASH_TABLE_INIT_SIZE 6
#define HASH_INDEX(ht, key) (hash_str((key)) % (ht)->size)

#if defined(DEBUG)
#  define LOG_MSG printf
#else
#  define LOG_MSG(...)
#endif

#define SUCCESS 0
#define FAILED -1

typedef struct _Bucket
{
char *key;
void *value;
struct _Bucket *next;
} Bucket;

typedef struct _HashTable
{
int size;		// 哈希表的大小
int elem_num;	// 已经保存元素的个数
Bucket **buckets;
} HashTable;

int hash_init(HashTable *ht);
int hash_lookup(HashTable *ht, char *key, void **result);
int hash_insert(HashTable *ht, char *key, void *value);
int hash_remove(HashTable *ht, char *key);
int hash_destroy(HashTable *ht);
#endif


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "hashtable.h"

static void resize_hash_table_if_needed(HashTable *ht);
static int hash_str(char *key);

int hash_init(HashTable *ht)
{
ht->size 		= HASH_TABLE_INIT_SIZE;
ht->elem_num 	= 0;
ht->buckets		= (Bucket **)calloc(ht->size, sizeof(Bucket *));

if(ht->buckets == NULL) return FAILED;

LOG_MSG("[init]\tsize: %i\n", ht->size);

return SUCCESS;
}

int hash_lookup(HashTable *ht, char *key, void **result)
{
int index = HASH_INDEX(ht, key);
Bucket *bucket = ht->buckets[index];

if(bucket == NULL) goto failed;

while(bucket)
{
if(strcmp(bucket->key, key) == 0)
{
LOG_MSG("[lookup]\t found %s\tindex:%i value: %p\n",
key, index, bucket->value);
*result = bucket->value;

return SUCCESS;
}

bucket = bucket->next;
}

failed:
LOG_MSG("[lookup]\t key:%s\tfailed\t\n", key);
return FAILED;
}

int hash_insert(HashTable *ht, char *key, void *value)
{
// check if we need to resize the hashtable
resize_hash_table_if_needed(ht);

int index = HASH_INDEX(ht, key);

Bucket *org_bucket = ht->buckets[index];
Bucket *tmp_bucket = org_bucket;

// check if the key exits already
while(tmp_bucket)
{
if(strcmp(key, tmp_bucket->key) == 0)
{
LOG_MSG("[update]\tkey: %s\n", key);
tmp_bucket->value = value;

return SUCCESS;
}

tmp_bucket = tmp_bucket->next;
}

Bucket *bucket = (Bucket *)malloc(sizeof(Bucket));

bucket->key	  = key;
bucket->value = value;
bucket->next  = NULL;

ht->elem_num += 1;

if(org_bucket != NULL)
{
LOG_MSG("[collision]\tindex:%d key:%s\n", index, key);
bucket->next = org_bucket;
}

ht->buckets[index]= bucket;

LOG_MSG("[insert]\tindex:%d key:%s\tht(num:%d)\n",
index, key, ht->elem_num);

return SUCCESS;
}

int hash_remove(HashTable *ht, char *key)
{
int index = HASH_INDEX(ht, key);
Bucket *bucket  = ht->buckets[index];
Bucket *prev	= NULL;

if(bucket == NULL) return FAILED;

// find the right bucket from the link list
while(bucket)
{
if(strcmp(bucket->key, key) == 0)
{
LOG_MSG("[remove]\tkey:(%s) index: %d\n", key, index);

if(prev == NULL)
{
ht->buckets[index] = bucket->next;
}
else
{
prev->next = bucket->next;
}
free(bucket);

return SUCCESS;
}

prev   = bucket;
bucket = bucket->next;
}

LOG_MSG("[remove]\t key:%s not found remove \tfailed\t\n", key);
return FAILED;
}

int hash_destroy(HashTable *ht)
{
int i;
Bucket *cur = NULL;
Bucket *tmp = NULL;

for(i=0; i < ht->size; ++i)
{
cur = ht->buckets[i];
while(cur)
{
tmp = cur;
cur = cur->next;
free(tmp);
}
}
free(ht->buckets);

return SUCCESS;
}

static int hash_str(char *key)
{
int hash = 0;

char *cur = key;

while(*cur != '\0')
{
hash +=	*cur;
++cur;
}

return hash;
}

static int hash_resize(HashTable *ht)
{
// double the size
int org_size = ht->size;
ht->size = ht->size * 2;
ht->elem_num = 0;

LOG_MSG("[resize]\torg size: %i\tnew size: %i\n", org_size, ht->size);

Bucket **buckets = (Bucket **)calloc(ht->size, sizeof(Bucket **));

Bucket **org_buckets = ht->buckets;
ht->buckets = buckets;

int i = 0;
for(i=0; i < org_size; ++i)
{
Bucket *cur = org_buckets[i];
Bucket *tmp;
while(cur)
{
// rehash: insert again
hash_insert(ht, cur->key, cur->value);

// free the org bucket, but not the element
tmp = cur;
cur = cur->next;
free(tmp);
}
}
free(org_buckets);

LOG_MSG("[resize] done\n");

return SUCCESS;
}

// if the elem_num is almost as large as the capacity of the hashtable
// we need to resize the hashtable to contain enough elements
static void resize_hash_table_if_needed(HashTable *ht)
{
if(ht->size - ht->elem_num < 1)
{
hash_resize(ht);
}
}


测试

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include "hashtable.h"

#define TEST(tcase) printf(">>> [START CASE] " tcase "<<<\n")
#define PASS(tcase) printf(">>> [PASSED] " tcase " <<<\n")

int main(int argc, char **argv)
{
HashTable *ht = (HashTable *)malloc(sizeof(HashTable));
int result = hash_init(ht);

assert(result == SUCCESS);

/* Data */
int  int1 = 10;
int  int2 = 20;
char str1[] = "Hello TIPI";
char str2[] = "Value";
/* to find data container */
int *j = NULL;
char *find_str = NULL;

/* Test Key insert */
TEST("Key insert");
hash_insert(ht, "KeyInt", &int1);
hash_insert(ht, "asdfKeyStrass", str1);
hash_insert(ht, "K13eyStras", str1);
hash_insert(ht, "KeyStr5", str1);
hash_insert(ht, "KeyStr", str1);
PASS("Key insert");

/* Test key lookup */
TEST("Key lookup");
hash_lookup(ht, "KeyInt", (void **)&j);
hash_lookup(ht, "KeyStr", (void **)&find_str);

assert(strcmp(find_str, str1) == 0);
assert(*j = int1);
PASS("Key lookup");

/* Test Key update */
TEST("Test key update");
hash_insert(ht, "KeyInt", &int2);
hash_lookup(ht, "KeyInt", (void **)&j);
assert(*j = int2);
PASS("Test key update");

TEST(">>>	 Test key not found		<<< ");
result = hash_lookup(ht, "non-exits-key", (void **)&j);
assert(result == FAILED);
PASS("non-exist-key lookup");

TEST("Test key not found after remove");
char strMyKey[] = "My-Key-Value";
find_str = NULL;
hash_insert(ht, "My-Key", &strMyKey);
result = hash_remove(ht, "My-Key");
assert(result == SUCCESS);

result = hash_lookup(ht, "My-Key", (void **)&find_str);
assert(find_str == NULL);
assert(result == FAILED);
PASS("Test key not found after remove");

PASS(">>>	 Test key not found		<<< ");

TEST("Add many elements and make hashtable rehash");
hash_insert(ht, "a1", &int2);
hash_insert(ht, "a2", &int1);
hash_insert(ht, "a3", &int1);
hash_insert(ht, "a4", &int1);
hash_insert(ht, "a5", &int1);
hash_insert(ht, "a6", &int1);
hash_insert(ht, "a7", &int1);
hash_insert(ht, "a8", str2);
hash_insert(ht, "a9", &int1);
hash_insert(ht, "a10", &int1);
hash_insert(ht, "a11", &int1);
hash_insert(ht, "a12", &int1);
hash_insert(ht, "a13", &int1);
hash_insert(ht, "a14", &int1);
hash_insert(ht, "a15", &int1);
hash_insert(ht, "a16", &int1);
hash_insert(ht, "a17", &int1);
hash_insert(ht, "a18", &int1);
hash_insert(ht, "a19", &int1);
hash_insert(ht, "a20", &int1);
hash_insert(ht, "a21", &int1);
hash_insert(ht, "a22", &int1);
hash_insert(ht, "a23", &int1);
hash_insert(ht, "a24", &int1);
hash_insert(ht, "a24", &int1);
hash_insert(ht, "a24", &int1);
hash_insert(ht, "a25", &int1);
hash_insert(ht, "a26", &int1);
hash_insert(ht, "a27", &int1);
hash_insert(ht, "a28", &int1);
hash_insert(ht, "a29", &int1);
hash_insert(ht, "a30", &int1);
hash_insert(ht, "a31", &int1);
hash_insert(ht, "a32", &int1);
hash_insert(ht, "a33", &int1);

hash_lookup(ht, "a23", (void **)&j);
assert(*j = int1);
hash_lookup(ht, "a30", (void **)&j);
assert(*j = int1);
PASS("Add many elements and make hashtable rehash");

hash_destroy(ht);
free(ht);

printf("Woohoo, It looks like HashTable works properly\n");

return 0;
}


PHP的哈希表实现

PHP内核中的哈希表是十分重要的数据结构,PHP的大部分的语言特性都是基于哈希表实现的, 例如:变量的作用域、函数表、类的属性、方法等,Zend引擎内部的很多数据都是保存在哈希表中的。

PHP中的哈希表实现在Zend/zend_hash.c中,还是按照上一小节的方式,先看看PHP实现中的数据结构, PHP使用如下两个数据结构来实现哈希表,HashTable结构体用于保存整个哈希表需要的基本信息, 而Bucket结构体用于保存具体的数据内容,如下:

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;
nTableSize字段用于标示哈希表的容量,哈希表的初始容量最小为8。首先看看哈希表的初始化函数:

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 *));
if (!tmp) {
return FAILURE;
}
ht->arBuckets = tmp;
} else {
tmp = (Bucket **) ecalloc_rel(ht->nTableSize, sizeof(Bucket *));
if (tmp) {
ht->arBuckets = tmp;
}
}

return SUCCESS;
}
例如如果设置初始大小为10,则上面的算法将会将大小调整为16。也就是始终将大小调整为接近初始大小的 2的整数次方。

为什么会做这样的调整呢?我们先看看HashTable将哈希值映射到槽位的方法,上一小节我们使用了取模的方式来将哈希值 映射到槽位,例如大小为8的哈希表,哈希值为100, 则映射的槽位索引为: 100 % 8 = 4,由于索引通常从0开始, 所以槽位的索引值为3,在PHP中使用如下的方式计算索引:

h = zend_inline_hash_func(arKey, nKeyLength);
nIndex = h & ht->nTableMask;


从上面的_zend_hash_init()函数中可知,ht->nTableMask的大小为ht->nTableSize -1。 这里使用&操作而不是使用取模,这是因为是相对来说取模操作的消耗和按位与的操作大很多。

设置好哈希表大小之后就需要为哈希表申请存储数据的空间了,如上面初始化的代码, 根据是否需要持久保存而调用了不同的内存申请方法。如前面PHP生命周期里介绍的,是否需要持久保存体现在:持久内容能在多个请求之间访问,而非持久存储是会在请求结束时释放占用的空间。 具体内容将在内存管理章节中进行介绍。

HashTable中的nNumOfElements字段很好理解,每插入一个元素或者unset删掉元素时会更新这个字段。 这样在进行count()函数统计数组元素个数时就能快速的返回。

nNextFreeElement字段非常有用。先看一段PHP代码:

<?php
$a = array(10 => 'Hello');
$a[] = 'TIPI';
var_dump($a);

// ouput
array(2) {
[10]=>
string(5) "Hello"
[11]=>
string(5) "TIPI"
}
PHP中可以不指定索引值向数组中添加元素,这时将默认使用数字作为索引, 和C语言中的枚举类似, 而这个元素的索引到底是多少就由nNextFreeElement字段决定了。 如果数组中存在了数字key,则会默认使用最新使用的key + 1,例如上例中已经存在了10作为key的元素, 这样新插入的默认索引就为11了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: