PHP 数组 与 golang map

697 阅读5分钟

前言

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 逻辑展示

hashtable.png

  • 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 逻辑展示

php数组展示图.png

  • 元素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.逻辑展示

golang底层map实现.png

  • hamp中buckets指向[]bmap,bmap是一个bucket。
  • bmap 里面存放这8个元素,bmap是一个连续的内存空间,头部存储的是tophash,然后是key值和value值
  • bmap最后存储的是overflow,指向溢出桶,主要是用来解决hash冲突的。因为每个bucket只能存8个元素,超过8个元素之后,额外的元素会放到另外一个bmap中,这些bmap统一个放在一个buckets中。

3.元素读取

  1. 首先获取计算出key的hash值,然后根据哈希值计算元素在哪个桶里面。
  2. 接着在取高位hash值,遍历出元素在桶的位置,然后通过指针运算计算出key和value的位置。
  3. 如果发现高位hash值没在桶内,那么在根据bmap最后的overflow 找到溢出桶,接着重复上的查询过程,直到到达元素或者,溢出桶为空。

总结

  • php5和php7底层都使用了hashtable的特性,php5通过双向链表实现的数组元素的有序,php7通过普通数组来维护数组元素顺序,通过中间映射表维护hash关系。
  • golang map底层也使用了hashtable特性,但是没记录元素的顺序。

文章中不对的知识点,请指出,向大家学习。