[李景山php] 深入理解PHP内核[读书笔记]--第三章:变量及数据类型--变量的结构和类型--PHP的哈希实现
2017-05-11 12:08
661 查看
对哈希表的需求远不止那么简单。对性能,灵活性都有不同的需求。下面我们看看PHP中的哈希表是怎么实现的。
nTableSize字段用于标示哈希表的容量,哈希表的初始容量最小为8,首先看看焊锡表的初始化函数
例如如果设置初始大小为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。这里使用&操作而不是使用取模,这是因为是相对来说取模操作的消耗和安慰与的操作大的多。
mask的作用就是将哈希值映射到槽位所能存储的索引范围内,例如:某个key的索引值是21,哈希表的大小为8,则mask为7,则求与时的二进制表示为: 10101 & 111 = 101也就是十进制的5。因为2的整数次方-1的二进制比较特殊:后面的N位的值都是1,这样比较容易能将值进行映射,如果是普通数字进行了二进制与之后会影响哈希值的结果。那么哈希函数计算的值的平均分布就可能出现影响。
设置好哈希表大小之后就需要为哈希表申请存储数据的空间了,如上面初始化的代码,根据是否需要持久保存而调用了不同内存的申请方法。如果前面PHP生命周期里介绍的,是否需要持久保存体现在:持久内容能在多个请求之间访问,而非持久存储是会在请求结束时释放占用的空间。具体内容将在内存管理章节中进行介绍。
HashTable中的nNumOfElements字段好理解,每插入一个元素或者unset删除元素时会更新这个字段。这样在进行count()函数统计数组元素个数就能快速的返回。
nNextFreeElement字段非常有用,先看一段PHP代码:
PHP中可以不指定索引值向数组中添加元素,这时将默认使用数字作为索引,和C语言中的枚举类似,而这个元素的索引到达是多少就由nNextFreeElement字段决定了。如果数组中存在了数字key,则会默认使用最新使用的key+1,例如上例中已经存在了10作为key的元素,这样新插入的默认索引就为11了。
如上面各个字段的注释。h字段保存哈希表key哈希后的值。这里保存的哈希值而不是哈希表中的索引值,这是因为索引值和哈希表的容量有直接关系,如果哈希表扩容了,那么这些索引还得重新进行哈希在进行索引映射,这也是一种优化手段。在PHP中可以使用字符串或者数字作为数组的索引。数字索引直接就可以作为哈希表的索引,数字也无需进行哈希处理。h字段后面的nKeyLength字段是作为key长度的标示,如果索引是数字的话,则nKeyLength为0,在PHP数组中如果索引字符串可以转化数字索引。
PHP的哈希实现
PHP内核中的哈希表是十分重要的数据结构,PHP的大部分语言特性都是基于哈希表实现的,例如:变量的作用域、函数表、类的属性、方法等,Zend引擎内部的很多数据都是保存在哈希表中的。数据结构及说明
上一节提到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 nNumOfElement;// 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; // Usee 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。这里使用&操作而不是使用取模,这是因为是相对来说取模操作的消耗和安慰与的操作大的多。
mask的作用就是将哈希值映射到槽位所能存储的索引范围内,例如:某个key的索引值是21,哈希表的大小为8,则mask为7,则求与时的二进制表示为: 10101 & 111 = 101也就是十进制的5。因为2的整数次方-1的二进制比较特殊:后面的N位的值都是1,这样比较容易能将值进行映射,如果是普通数字进行了二进制与之后会影响哈希值的结果。那么哈希函数计算的值的平均分布就可能出现影响。
设置好哈希表大小之后就需要为哈希表申请存储数据的空间了,如上面初始化的代码,根据是否需要持久保存而调用了不同内存的申请方法。如果前面PHP生命周期里介绍的,是否需要持久保存体现在:持久内容能在多个请求之间访问,而非持久存储是会在请求结束时释放占用的空间。具体内容将在内存管理章节中进行介绍。
HashTable中的nNumOfElements字段好理解,每插入一个元素或者unset删除元素时会更新这个字段。这样在进行count()函数统计数组元素个数就能快速的返回。
nNextFreeElement字段非常有用,先看一段PHP代码:
<?php $a = array(10 => 'hello'); $a[] = 'world'; var_dump($a); //output array(2){ [10]=>string(5)'Hello' [11]=>string(5)'world' }
PHP中可以不指定索引值向数组中添加元素,这时将默认使用数字作为索引,和C语言中的枚举类似,而这个元素的索引到达是多少就由nNextFreeElement字段决定了。如果数组中存在了数字key,则会默认使用最新使用的key+1,例如上例中已经存在了10作为key的元素,这样新插入的默认索引就为11了。
数据容器:槽位
下面看看保存哈希表数据的槽位数据结构体: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 *pList;//同一个哈希bucket的上一个元素 // 保存当期值所对于的key字符串,这个字段只能定义在最后,实现变长结构体 char arKey[1]; }Bucket;
如上面各个字段的注释。h字段保存哈希表key哈希后的值。这里保存的哈希值而不是哈希表中的索引值,这是因为索引值和哈希表的容量有直接关系,如果哈希表扩容了,那么这些索引还得重新进行哈希在进行索引映射,这也是一种优化手段。在PHP中可以使用字符串或者数字作为数组的索引。数字索引直接就可以作为哈希表的索引,数字也无需进行哈希处理。h字段后面的nKeyLength字段是作为key长度的标示,如果索引是数字的话,则nKeyLength为0,在PHP数组中如果索引字符串可以转化数字索引。
未完,待续……………………………
[b]________[/b]–相关文章推荐
- [李景山php] 深入理解PHP内核[读书笔记]--第三章:变量及数据类型--变量的结构和类型--类型提示的实现
- [李景山php] 深入理解PHP内核[读书笔记]--第三章:变量及数据类型--变量的结构和类型--常量
- [李景山php] 深入理解PHP内核[读书笔记]--第三章:变量及数据类型--变量的结构和类型
- [李景山php] 深入理解PHP内核[读书笔记]--第三章:变量及数据类型--变量的结构和类型--链表
- [李景山php] 深入理解PHP内核[读书笔记]--第三章:变量及数据类型--变量的结构和类型--HashTable
- [李景山php] 深入理解PHP内核[读书笔记]--第三章:变量及数据类型--变量的结构和类型--预定义变量
- [李景山php] 深入理解PHP内核[读书笔记]--第三章:变量及数据类型--变量的结构和类型--HashTable-1
- [李景山php] 深入理解PHP内核[读书笔记]--第三章:变量及数据类型--变量的结构和类型--静态变量
- [李景山php] 深入理解PHP内核[读书笔记]--第三章:变量及数据类型--数据类型转换
- [李景山php] 深入理解PHP内核[读书笔记]--第三章:变量及数据类型--变量的作用域
- [李景山php] 深入理解PHP内核[读书笔记]--第三章:变量及数据类型--变量的赋值和销毁
- [李景山php] 深入理解PHP内核[读书笔记]--第三章:变量及数据类型--简略
- [李景山php] 深入理解PHP内核[读书笔记]--第三章:变量及数据类型--global语句
- [李景山php] 深入理解PHP内核[读书笔记]--第三章:变量及数据类型--变量的生命周期
- 深入理解PHP内核[读书笔记]--第三章:变量及数据类型--变量的作用域
- 深入理解PHP内核[读书笔记]--第三章:变量及数据类型--变量的作用域
- [李景山php] 深入理解PHP内核[读书笔记]--第五章:类和面向对象 --类的结构和实现
- [李景山php] 深入理解PHP内核[读书笔记]--第二章:用户代码执行--SAPI概述-PHP中的CGI实现
- [李景山php] 深入理解PHP内核[读书笔记]--第四章:函数的实现 --函数的定义
- [李景山php] 深入理解PHP内核[读书笔记]--第四章:函数的实现 --函数的参数-1