浅析PHP数组的实现

一. PHP数组特点介绍

       php数组可谓是php的核心,其key=>value的存储结构,让我们处理数据可以游刃有余,并且在一般情况下增删查改都是O(1)的时间复杂度,并表现出一些特点让我们对其内部的结构充满了好奇。

「数组内存申请不需要自己控制」:数组的内存申请和释放是内部控制的,不需要人为的去干预。

「兼容关联数组和索引数组」:数组可以以数字为下标进行操作,也可以以字符串为下标进行操作,其实他俩本质上都是key=>value的格式。

「可以无key值存储」:像a[] = 10; 这样去存储一个value值,事实上他也是有key值的。

「不会出现数组越界的问题」:不会像我们操作C/C++等语言一样,要注意数组的边界,一定不能正向或者逆向越界。在php中不会有这些问题的存在。

「value值无严格的定义和统一」:这里就是php内部自己实现了一个范型。

「foreach遍历数组与插入顺序有关」:插入顺序和遍历输出一致。

二. PHP数组结构的演变

这个地方主要讲PHP5.6=>PHP7.0=>PHP7.1的演变过程


PHP5.6的数组结构

我们可以看到,数组本质就是一个hashtable结构,左侧的0~nTablemask便是hash下标,而后面有一个双向链表,便是我们通常所说的hash冲突的链地址法。

而绿色的双向链表,则是foreach遍历用的,这个地方用双向链表,主要是为了逆序访问来用的,遍历的时候,就是从pListHead开始不断的next便可以遍历所有的元素。这样要比下标遍历hashtable快得多。

但是这个结构有很明显的缺点:

①. 每次插入/删除元素都要申请/释放内存,这样会严重的导致内存碎片化,降低内存的使用率。

②. 再者,每次插入元素的时候都要去申请内存,会有一次寻址的过程,时间效率低。

③. 指针结构很复杂,有大量的指针操作。


PHP7.0的数组结构

针对上面5.6所有的问题,提出了如上的结构。arData结构便是我们插入数据的结构,是一段连续的内存,在初始化/数组扩充时一次性内存申请好,这样内存便不会产生碎片化问题,并且我们看,hash冲突的时候是单链表,并且没有插入元素时候的双向链表,因为我们可以直接扫描数组便可以顺序/逆序访问插入的数据。


PHP7.1的数组结构

如上是最新的数据结构,最主要的区别是把7.0的两段内存转化为一段内存,算是在内存上的继续优化。他的内存申请是从下标-8位置申请的,然后移动指针到key1位置。

三. PHP7.0数组源码实现

3.1 PHP7.0数组的主要数据结构


Bucket结构

Bucket结构便是我们所说的保存插入数据的结构。主要包括:key(字符串,如果是数字下标,转化位字符串), value, h(只会计算一次,如果是数组下标,直接把key作为h)。


PHP数组的结构

这便是PHP7.0的数组结构,我后面讲全部是用7.0结构讲,比较清晰。nNextFreeElement是无key值存储时使用的数据,用来分配key值。

3.2 PHP数组的hash方法


PHP hash源码

如上是php的hash源码,看一眼容易把人瞎蒙,然而把它简化一下便是:


PHP 源码转化

简单讲一下。其中源码中把 hash*33 转化为 (hash<<5) + hash

在一个就是,使用了循环展开策略,减少不必要的循环判断,在一个是连续的执行相同的指令,提高指令的cache命中率。可以看着:https://en.wikipedia.org/wiki/Loop_unrolling

那么计算出hash值来之后,并不是我们想要的下标,因为很有可能超过了hashmask值,所以需要一步计算来得到index。

•7.0:

hashmask = tablesize – 1;   //tablesize一定是2的指数倍,和申请内存有关系,后面讲

h = hashcode & hashmask;  //计算index

hashmask=7(10)=111(2)       //十进制下的7转为二进制下的111

0<=h<8                                 //所有值 & hashmask 一定在0~7之间

•7.1:

hashmask = -tablesize;

h = hashcode | hashmask;

hashmask=-8(10)=11111000(2补码)

-8<=h<=-1

7.1的结构因为是hash负下标,所以用的另一种计算策略,大同小异。

3.3 PHP数组插入数据


PHP插入数据的流程图

大致流程如上,看一下就好。讲一下插入数据的结构变化。


插入数据结构变化

在我们插入key6这个key值之后,在arData数据往后排,我们假设他与key1的hash冲突,所以使用链地址法,直接插入到链表首部。

3.4 PHP数组删除数据

在上面的结构,假如我们删除key1,会有两种考虑。

①. 删除该元素之后,我们是否要保证空间的利用率,把后面的数据移动,覆盖当前位置。

②. 删除元素之后,直接把该位置空出。

其实第一种策略是不可接受,因为这样会导致时间复杂度退化为O(n),而第二种策略,我们控制当前位置之后,我们可以在内存扩充时,把这些空位置给删除掉即可。

PHP数组删除数据

3.4 PHP数组内存申请及rehash

假设我们要在如下结构中插入key9

PHP 数组结构

发现当前arData已经满了,所以我们申请新内存(初始大小位2,以2的倍数不断扩充),把所有数据以内存的形式copy过去,然后扫描数组,把无用的数据给删除掉(用移动后面元素的策略)。因为nTableMask数值改变了然后进行rehash。

这里有一个点值得提一下:把原数据copy过去的时候,我们可以选择遍历原先的数组,把有用的数据copy过去,也可以先把原内存的数据复制到新内存的位置,然后去遍历数组去移动数据。PHP实现选择了第二种,这样考虑应该是我们删除元素的次数比较少,直接以内存的形式copy数据要比遍历copy的速度快,在权衡之下选择了第二种。


插入数据之后

结构变化如上图。

四. PHP数组的可优化点

4.1 PHP数组的退化案例

本案例参考的鸟哥的博客。


攻击代码

大概测了一下

运行结果

我们可以看到,时间效率差别很大。我们来分析一下为什么会这样。

•插入元素:key=0,1*65536,2*65536,….,65535*65536

•插入元素特点:h=key&65535=0,生成绝对的hash冲突导致退化1*****00000000&0****011111111=0

•大致的插入计算次数:1+2+3+4+5+…+65536(每次插入都会冲突,从而会去查询是否有相同的key值,所以会有扫描的次数总和如此)

•导致插入元素时间复杂度退化为O(n*(1+n)/2)=O(n^2)

那么65536^2在计算机1s处理10^8运算的情况下,大概36s以上,所以运行结果算是可预期的。


hash冲突的结构

4.2 可进行优化的策略

我们参考Java中hashtable的实现来使用红黑树解决这个问题。

•①.设定链表冲突长度的阀值

•②.当链表长度超过阀值的时候,以key值为键值进行红黑树的转化,key全是字符串,所以我们可以以key的字典序进行排序

•③.Hashtable的指针指向树根

•④.增删查改,不断对红黑树结构进行调整

•⑤.使增删查改时间复杂度达到最差为O(logn)

我们来看下结构的变化为:


加入红黑树后的结构变化

如果不了解红黑树,可以去参考一下《算法导论》中的详解

五. PHP数组的总结

「数组内存申请不需自己控制」

hashtable内存大小与arData大小相同,并且一定为2的指数倍不断扩展

「兼容关联数组和索引数组」

索引是hash值同时也是key值,转化为关联数组,关联数组用hashtable实现

「可以无索引(key值)存储」

无索引存储时,会自动为其分配最后一个没有被使用过的下标来作为key和hash值进行存储

「不会出现数组越界的情况」

索引即hash值,用无符号长整形存储,不会出现负向越界

索引的大小与内存的大小没有绝对的关系,不会出现正向越界

「value值无严格的定义或统一」

php对value值进行了范型处理

「foreach遍历数组与插入顺序有关」

foreach是对arData遍历,而arData是顺序存储的

用foreach比用for进行下标访问永远要快

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容