记一个小问题的优化的思考

494 阅读4分钟

问题源自于一次群友求助:

image.png

首先看了下 是用的push这个方法,方法没什么问题,因为对比其他的方法 如unshift向前填充, splice中间插入,后者会移动其他值的位置 我们用splice来举个例子 const arr = [1, 2, 3] arr.splice(1, 0, 'a') 内部是这么执行的

image.png

image.png

image.png

image.png

image.png

内部其实是分了四步,但push没有这个问题,因为push只有两步

const arr = [1, 2, 3] 
arr.push('a')  

image.png

image.png

看起来循环内部没有任何问题 这时我想到 大量循环中 每次都要拓展 赋值 拓展 赋值, 如果一次性拓展完,再循环赋值呢

image.png

image.png

原理也很简单,如果是push,相当于重新分配一百万次内存空间,赋值一百万次 后者只需要分配一次内存空间,赋值一百万次
随着循环次数增加,差距也会越拉越大

image.png

image.png

经过验证,确实可以显著的节省时间

问题:如果不是整数 而是字符串呢?

所以这里要补充一下,提前开辟空间有显著提升,其实是有一个前提的,当时的场景是从arrayBuffer中提取二进制数据,所以能够保证都是整数的number类型,且填充次数非常的大
如果不能保证全都是整数,那么差异将不会很大
我们重新定义一下函数,与之前的区别是,这次填充的是字符串

image.png

image.png

可以看到差距并没有那么显著

image.png

问题:如果不循环填充很多次呢?

我们再试试减少填充次数, 只填充10个

image.png

image.png

可以看到提前开辟空间反倒性能更差

image.png

问题: 如果在只循环10次的基础上,换回整数呢?

再来看看整数的性能对比

image.png

image.png

可以看到虽然没有字符串那么夸张,但也是呈下降趋势的

image.png

为什么会有这种现象?

这是因为在v8中数组类型分为六种
PACKED_SMI_ELEMENTS
PACKED_DOUBLE_ELEMENTS
PACKED_ELEMENTS
HOLEY_SMI_ELEMENTS
HOLEY_DOUBLE_ELEMENTS
HOLEY_ELEMENTS

PACKED的意思是连续有值的,HOLEY的意思是稀疏的 SMI代表整数,DOUBLE代表浮点数,没有则代表空类型,空类型一般是字符串或者函数

我们每次新声明一个空数组的时候,都是 PACKED_SMI_ELEMENTS 类型,代表的意思是连续有值的整数数组,该类型是最快的模式

const arr = [] // PACKED_SMI_ELEMENTS

如果按顺序插入整数,v8会自动扩容,类型不变\

const arr = [] // PACKED_SMI_ELEMENTS 
arr.push(1) // PACKED_SMI_ELEMENTS

直接创建全都是整数的连续数组,类型也是一样\

const arr = [1, 2, 3] // PACKED_SMI_ELEMENTS

但如果插入了一个浮点数,v8就会进行降级处理,类型就会变成 PACKED_DOUBLE_ELEMENTS, 代表的意思是连续有值的浮点数数组\

const arr = [] // PACKED_SMI_ELEMENTS
arr.push(1.1) // PACKED_DOUBLE_ELEMENTS

如果突然访问超过数组长度的项,就会继续降级,类型变成HOLEY_DOUBLE_ELEMENTS,代表的意思是浮点数稀疏数组\

const arr = [] // PACKED_SMI_ELEMENTS
arr.push(1.1) // PACKED_DOUBLE_ELEMENTS
arr[100] = 100 // HOLEY_DOUBLE_ELEMENTS

如果此时插入一个字符串或者函数,就会变成最慢的类型 HOLEY_ELEMENTS,代表的意思是稀疏数组\

const arr = [] // PACKED_SMI_ELEMENTS
arr[100] = 'abc' // HOLEY_ELEMENTS

值得说明的是:降级过程是不可逆的
为了证明这点,我用v8-debug调试了一下

image.png

此时空数组状态是PACKED_SMI_ELEMENTS类型

image.png

经过填充一次浮点数之后,成功降级成PACKED_DOUBLE_ELEMENTS类型

image.png

在填充浮点数之后立刻删除,但数组的类型却并没有变回来
另外有一张图可以提供参考

image.png

通过这张图的关系可以看到一些规律:PACKED 只会变成更糟的 HOLEY,SMI 只会往更糟的 DOUBLE 和空类型走,且这两种变化都不可逆。

再回到我们这次优化中,首先我们的操作是连续填充整数
第一种常规push,类型是PACKED_SMI_ELEMENTS,虽然性能是最快的,但缺点是每次都要进行扩容才能赋值 第二种先扩容再填充,由于长度发生了变化,降级为HOLEY_SMI_ELEMENTS,稀疏整数数组,虽然不是最快的,但也只比PACKED_SMI_ELEMENTS慢一点

image.png

所以在大量循环中,连续填充整数,先行扩容的效率要更高

但是当填充的是字符串时,情况就不一样了
第一种常规push的类型是PACKED_ELEMENTS

image.png

第二种直接降级成性能最差的HOLEY_ELEMENTS

image.png

HOLEY_ELEMENTS虽然只和PACKED_ELEMENTS差了一级,但是性能差的却不是一丁半点,众所周知,JS是动态控制内存,C++却不是,在V8中每次都要重新查询内存的分配,这里的查询需要根据数据的类型去查,PACKED_ELEMENTS代表的是连续的,而HOLEY_ELEMENTS则没有任何条件

v8代码参考处

image.png

image.png

image.png

所以基于大量操作,且值都为整数的情况下,我给出了如下建议:

image.png

image.png

下期预告

image.png

百万级的循环赋值对象,循环过程中附带一个查重功能。从赋值方式来看貌似没什么问题,我首先想到的是用worker去做计算,避免阻塞。但我不但想避免阻塞,还想减少等待时长,于是展开了一番调研, 调研结果敬请期待

image.png