最近在面试一些候选人的过程中,对于具有php经验的候选人,经常会问php7 相对于php5做了哪些改进和提升,进而会问php7的数组到底比php5的数组在底层实现上有哪些改进和提升。对于这个问题很多候选人都回答的不好。今天在这里给大家分享下我对这个问题的理解
php数组
php5的数组和其他语言如python、go语言对于数组的定位是不一样的,以Go语言为例,Go的数组就是我们传统理解的数组,但是PHP的数组是兼具数组、哈希map的结合。所以实现方式上差别也比较大。
Php 数组底层是一个hash表,说到hash 就不得不提到hash碰撞,解决方案无外乎开链法和开放地址法,php使用的是前一种方法。那么一般面试中,数组深度上会进一步问以下问题:
- 数组实现为什么要用hashmap?
- hashmap 如何保证插入顺序和遍历顺序的一致
- foreach 和 for 有啥区别
上面3个问题是面试中关于数组面试的常见的延伸问题。 第一个问题很好理解,因为php数组不光是传统意义上的数组,还是kv结构的集合,arr[0] 或者 arr["a"] 都是可以的,如果不是kv结构 arr["a"]的时间复杂度就是O(n)了。
既然php数组是map结构,大家知道,map的插入和遍历顺序不保证一致,那如何保证一致呢,这就是第2个问题了。 答案是:php的数组不光是一个hashmap,还是一个双向链表,链表的顺序就是插入的顺序,遍历时是按照这个双向链表来遍历的
第3个问题,在弄清楚1和2问题后,第三个问题就比较好理解了。 foreach 遍历走的双向链表,而for是根据下标遍历,下标通过hash函数转化为实际的存储位置(php里面叫"bucket",一个"bucket"有多个元素),然后才能找到元素。 所以foreach 比 for 要快也是理所当然的了。
好了说了这么多,进入正题,我们看下php5的数组和php7的数组在实现上到底哪里不一样呢
php5数组
php5数组实现是一个hashmap 又是一个双向链表
上图中 pListHead 是数组头指针,pListTail是数组尾指针,Bucket是hash之后具有相同hash key的元素的链表。红色箭头线是双向链表的指针,黑色箭头线是hash冲突的开链法的指针
数组结构体定义:
typedef struct _Bucket
{
char *key;
void *value;
struct _Bucket *next;
} Bucket;
typedef struct _HashTable
{
int size;
int elem_num;
Bucket** buckets;
} HashTable;
进一步阅读:www.php-internals.com/book/?p=cha…
php7数组
先看一下数组的逻辑结构
$arr["a"] = 1;
$arr["b"] = 2;
$arr["c"] = 3;
$arr["d"] = 4;
unset($arr["c"]);
数组的逻辑结构
由上图可以看出,php7数组的元素是顺序存储的(php5不是),在元素存储区域的前面,留了一块区域存储hash值到存储位置的映射。例如,一个元素(key=a),元素存在了第0位置,a hash之后的位置为-4,所以-4位置存的就是0,以此类推。对于hash碰撞的解决依赖了zval的next指针,这里就不展开了。
数组的结构体定义,大家参考下
//Bucket:散列表中存储的元素
typedef struct _Bucket {
zval val; //存储的具体value,这里嵌入了一个zval,而不是一个指针
zend_ulong h; //key根据times 33计算得到的哈希值,或者是数值索引编号
zend_string *key; //存储元素的key
} Bucket;
//HashTable结构
typedef struct _zend_array HashTable;
struct _zend_array {
zend_refcounted_h gc;
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar flags,
zend_uchar nApplyCount,
zend_uchar nIteratorsCount,
zend_uchar reserve)
} v;
uint32_t flags;
} u;
uint32_t nTableMask; //哈希值计算掩码,等于nTableSize的负值(nTableMask = -nTableSize)
Bucket *arData; //存储元素数组,指向第一个Bucket
uint32_t nNumUsed; //已用Bucket数
uint32_t nNumOfElements; //哈希表有效元素数
uint32_t nTableSize; //哈希表总大小,为2的n次方
uint32_t nInternalPointer;
zend_long nNextFreeElement; //下一个可用的数值索引,如:arr[] = 1;arr["a"] = 2;arr[] = 3; 则nNextFreeElement = 2;
dtor_func_t pDestructor;
};
详细讲解大家参考文献1
结论
php7的数组是顺序存储的,php5不是,因为数组的访问具有局部性原理(遍历场景)所以顺序存储的性能优势还是很大的(具体原因就不展开了,大家可以看下计算机系统结构,链接:book.douban.com/subject/700…
其实面试官考察的不光是什么,更多的是为什么?只有知道别人实现的背景、解决的思路等才能体现出候选人的优秀。
声明 php7 数组结构参考了参考文献1中的图
编程、面试交流
参考文献: