php5 数组实现和php7数组实现有哪些不同

1,902 阅读4分钟

最近在面试一些候选人的过程中,对于具有php经验的候选人,经常会问php7 相对于php5做了哪些改进和提升,进而会问php7的数组到底比php5的数组在底层实现上有哪些改进和提升。对于这个问题很多候选人都回答的不好。今天在这里给大家分享下我对这个问题的理解

php数组

php5的数组和其他语言如python、go语言对于数组的定位是不一样的,以Go语言为例,Go的数组就是我们传统理解的数组,但是PHP的数组是兼具数组、哈希map的结合。所以实现方式上差别也比较大。

Php 数组底层是一个hash表,说到hash 就不得不提到hash碰撞,解决方案无外乎开链法和开放地址法,php使用的是前一种方法。那么一般面试中,数组深度上会进一步问以下问题:

  1. 数组实现为什么要用hashmap?
  2. hashmap 如何保证插入顺序和遍历顺序的一致
  3. 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 又是一个双向链表

php5数组逻辑结构

上图中 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数组逻辑结构

由上图可以看出,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中的图

编程、面试交流

章鱼编程

参考文献:

1、github.com/pangudashu/…

2、www.php-internals.com/book/?p=cha…