JS Array : 数据容器界的戏精

65 阅读3分钟

对比 C 等语言实现,JS array 并非静态数组,而是根据场景高度优化的动态结构

fast element 快速元素 (密集数组)

当数组元素类型一致且连续的时候(纯数字/纯字符串)

元素连续存储在内存中,通过索引直接访问(由于是连续的,对于操作系统只需要通过下标加开始的内存编号就能够获取到对应位置)时间复杂度是 O(n)

dictionary elements 慢速元素 (稀疏数组)

当数组稀疏,或者混合了多种数据类型的时候,底层会转换成哈希表,此时访问效率有所降低,但是时间复杂度上任然保持 O(1)

快慢元素对比

慢速元素索引访问为什么效率会降低

  • 相比连续存储,哈希需要求哈希键值去找哈希槽,这步有一些开销
  • 此外,哈希表还要处理冲突的情况,可能需要遍历链表或者继续向下访问

查找的效率 (includes)对比

  • 查询是线性访问的,但是哈希表需要去跳转哈希槽,内存上是非连续的,所以效率不如快速元素,但是时间复杂度任然是O(n)
  • 密集数组的内存访问是连续的,所以能够更加充分的利用上操作系统硬件级别的缓存,稀疏数组会频繁的出发缓存行的替换

特殊优化

一句话,数组中使用更少的类型,更加密集的存储有利于利用引擎的设计优势

  1. 元素类型特化

使用纯数字数组,存浮点数可能会采用更加高效的存储类型以优化内存占用

使用对象数组,会单独的连续存放对象的指针(对象本身是不连续的)

  1. 隐藏类

对象数组如果元素的结构一致 ****, 引擎会复用隐藏类,确保内存中的字段偏移量固定,从而提高缓存一致性,提高对CPU缓存的使用效率

  1. 内存 预分配

动态扩容时,快速元素会预留额外容量(如当前长度的 1.5 倍),减少后续扩容时的内存碎片化。

  1. 写时复制

const a = new Array(1e6).fill(0); // 分配大数组
const b = a.slice(0,100); // 共享底层存储
b[0] = 1; // 触发实际复制

所以如果我们只是访问数组而不是修改他,我们可以放心使用 slice

对比 C 语言

JS Array

  • 动态类型(所以不限定类型)
  • 自动垃圾回收
  • 动态扩缩
  • 快慢模式有所区别

C 数组

  • 静态类型(所以限定类型)
  • 手动生命周期管理
  • 固定长度声明
  • 严格连续映射物理地址

CPU 缓存工作机制

CPU 缓存 (Cache)是硬件级别的快速内存,用于减少主存的延迟

  • 按照固定的大小(64Byte通常)保存到缓存行中
  • 具有空间局部性,倾向于优化相邻的内存地址数据
  • prefectching 预取机制,CPU 预测后序访问模式,提前加载数据到缓存中
  • 对于 JS V8 引擎会自动的去对齐当前的缓存空间以优化存储

笔者才疏学浅,各位读者多多担待,不吝赐教。