前言
php 做了多年,然后接触了go的项目,在go的项目用map来代替php的数组,因为它们都是key,value 键值对的,用的时候发现golang的map并不能保证元素写的顺序性。
内容
下面我对php数组和golang map的底层结构来分析,为什么php数组是有序的,然后golang map是无顺的。
PHP 数组实现
有序php7 对php5 数组做了重新设计,所有下面分别介绍。
1.php5 底层实现
1.1 底层代码
代码位置 Zend/zend_hash.h
typedef struct bucket {
ulong h; //数组key hash之后的索引值或者数字索引值
uint nKeyLength; //数组key的字符串长度,如果是数字索引值那为0
void *pData;//实际数据存储的地址,一般是数据的副本,如果是指针数据,则同*pDataprt,指向存储数据zval
void *pDataPtr;//引用数据存储的地址,如果值指针数据,那么指向同*pData ,指向存在数据的zval
struct bucket *pListNext; //数组中下一个元素地址
struct bucket *pListLast; //数组中上一个元素地址
struct bucket *pNext; //同一个桶中下一个元素地址
struct bucket *pLast; //同一个桶中上一个元素地址
const char *arKey;
} Bucket;
typedef struct _hashtable {
uint nTableSize; //哈希表中Bucket的槽的数量,初始值为8,每次resize时以2倍速度增长
uint nTableMask;
uint nNumOfElements; //数组元素的个数
ulong nNextFreeElement; //下一个数字索引的位置
Bucket *pInternalPointer;
Bucket *pListHead; //数组中第一个元素地址
Bucket *pListTail; //数组中最后一个元素地址
Bucket **arBuckets; //指针数组,数组中每个元素都是指针,存储hash数组
dtor_func_t pDestructor;
zend_bool persistent;
unsigned char nApplyCount;
zend_bool bApplyProtection;
#if ZEND_DEBUG
int inconsistent;
#endif
}
1.2 逻辑展示
- pListNext,pListLast 来维护数组顺序结构
- pNext,pLast 解决哈希冲突。图书蓝色的bucket 是表示哈希冲突。
- 通过哈希函数可以计算出元素的索引值,通过索引值可以运算定位到元素。
2.php7 底层代码实现
2.1 底层结构
代码位置 Zend/zend_types.h
typedef struct _Bucket {
zval val;
zend_ulong h; /* hash value (or numeric index) */
zend_string *key; /* string key or NULL for numerics */
} Bucket;
typedef struct _zend_array HashTable;
struct _zend_array {
zend_refcounted_h gc;
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar flags,
zend_uchar _unused,
zend_uchar nIteratorsCount,
zend_uchar _unused2)
} v;
uint32_t flags;
} u;
uint32_t nTableMask;
Bucket *arData;
uint32_t nNumUsed;
uint32_t nNumOfElements;
uint32_t nTableSize;
uint32_t nInternalPointer;
zend_long nNextFreeElement;
dtor_func_t pDestructor;
};
2.2 逻辑展示
- 元素key用过哈希函数计算中索引值,然后在通过索引值找出映射表中的位置,映射表中对于索引值是对应buckets中元素为位置。
- 如果出现hash冲突,那么通过拉链法结果含量冲突,如上图中红线
- 中间映射表是用来通过索引值来快速定位元素
- buckets 按照元素插入顺序,记录元素顺序。
3. php5和php7数组实现总结
- php5数组通过hashtable和双向链表实现,hashtable提供可通过key快速查找元素,双向链表保证数据遍历有序
- php7通过在中间映射表通过索引值定位数组元素,通过buckets数组顺维护数组顺序
golang map实现
1.底层代码
代码位置 runtime/map.go
type hmap struct {
count int //键值对的数量
flags uint8 //状态标识,比如正在被写、buckets和oldbuckets在被遍历、等量扩容(Map扩容相关字段)
B uint8 2^B=len(buckets)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash因子
buckets unsafe.Pointer // 指向一个数组(连续内存空间),数组的类型为[]bmap,bmap类型就是存在键值对的结构下面会详细介绍,这个字段我们可以称之为正常桶。
oldbuckets unsafe.Pointer // 扩容时,存放之前的buckets
nevacuate uintptr // 分流次数,成倍扩容分流操作计数的字段(Map扩容相关字段))
extra *mapextra // 溢出桶结构,正常桶里面某个bmap存满了,会使用这里面的内存空间存放键值对
}
type mapextra struct {
overflow *[]*bmap //溢出桶buckets
oldoverflow *[]*bmap //扩容时,存放之前的溢出桶buckets
nextOverflow *bmap //指向溢出桶里下一个可以使用的bucket
}
type bmap struct {
tophash [bucketCnt]uint8 //key值的高位hash
}
2.逻辑展示
- hamp中buckets指向[]bmap,bmap是一个bucket。
- bmap 里面存放这8个元素,bmap是一个连续的内存空间,头部存储的是tophash,然后是key值和value值
- bmap最后存储的是overflow,指向溢出桶,主要是用来解决hash冲突的。因为每个bucket只能存8个元素,超过8个元素之后,额外的元素会放到另外一个bmap中,这些bmap统一个放在一个buckets中。
3.元素读取
- 首先获取计算出key的hash值,然后根据哈希值计算元素在哪个桶里面。
- 接着在取高位hash值,遍历出元素在桶的位置,然后通过指针运算计算出key和value的位置。
- 如果发现高位hash值没在桶内,那么在根据bmap最后的overflow 找到溢出桶,接着重复上的查询过程,直到到达元素或者,溢出桶为空。
总结
- php5和php7底层都使用了hashtable的特性,php5通过双向链表实现的数组元素的有序,php7通过普通数组来维护数组元素顺序,通过中间映射表维护hash关系。
- golang map底层也使用了hashtable特性,但是没记录元素的顺序。
文章中不对的知识点,请指出,向大家学习。