数组里混进一个 0.5,遍历慢了 4 倍

举报
yd_293143657 发表于 2026/11/12 01:14:18 2026/11/12
【摘要】 压测时遇到一个怪事:同样一段求和循环,数组里的数全换成浮点之后,耗时涨了 4 倍。存储没变、循环没变、数据量没变,变的只是一个元素的类型。 实验2000 万个元素,循环体 acc = (acc + a[i]) | 0,每种数组各跑 3 轮取最快:数组元素种类耗时每元素倍数PACKED_SMI(全整数)22.9 ms1.15 ns1.0xPACKED_DOUBLE(混一个 0.5)96.3 m...

压测时遇到一个怪事:同样一段求和循环,数组里的数全换成浮点之后,耗时涨了 4 倍。存储没变、循环没变、数据量没变,变的只是一个元素的类型。

实验

2000 万个元素,循环体 acc = (acc + a[i]) | 0,每种数组各跑 3 轮取最快:

数组元素种类 耗时 每元素 倍数
PACKED_SMI(全整数) 22.9 ms 1.15 ns 1.0x
PACKED_DOUBLE(混一个 0.5) 96.3 ms 4.82 ns 4.2x
PACKED_ELEMENTS(混一个字符串) 160.1 ms 8.01 ns 7.0x
Float64Array 92.1 ms 4.61 ns 4.0x

机制

V8 的数组背后是一块按元素种类(elements kind)分配的内存:

  • PACKED_SMI_ELEMENTS:每个槽直接存 32 位小整数
  • PACKED_DOUBLE_ELEMENTS:每个槽存一个 double,整块 8 字节对齐
  • PACKED_ELEMENTS:每个槽存指针,指向堆上的对象

这个种类只能往下降,不能往回升。给 a[10000000] = 0.5 赋一个浮点,V8 要新建一块 double 数组、把两千万个值逐个搬过去;之后再写回整数也不会退回 SMI。再混进一个字符串,就整体变成指针数组,读出来的每个数字都要多做一次拆箱。

反转:慢的不是存储

第二轮实验,把循环体从 (acc + a[i]) | 0 换成 acc += a[i]:

数组元素种类 取整累加 浮点累加
PACKED_SMI 22.9 ms 32.6 ms
PACKED_DOUBLE 96.3 ms 24.4 ms
Float64Array 92.1 ms 23.8 ms

DOUBLE 从 96.3 掉到 24.4,比 SMI 走浮点累加还快。所以那 4 倍不是「double 存储比 SMI 存储贵」,而是循环体在整数模式下必须每次把 double 截回 int32 —— 每轮多一次读、转、写。存储本身在两种累加方式下是一个价。

SMI 反过来也有坑:acc += a[i] 累加到 2000 万 × 1023 ≈ 2×10^10 时超出 int32 范围,累加器自己被提升成 double,于是整数数组走浮点累加反而更慢(32.6 > 22.9)。两个方向都指向同一件事:成本在类型转换,不在数组。

内存:字节数一样,GC 待遇不同

三种表示都是 2000 万 × 8 B ≈ 152 MB。实测驻留:

PACKED_SMI      堆 +152 MB   external +  0 MB
PACKED_DOUBLE   堆 +153 MB   external +  0 MB
Float64Array    堆 +  0 MB   external +153 MB

Typed array 的数据在 GC 堆之外,ArrayBuffer 的 backing store 记在 external。152 MB 的 GC 堆每轮标记-清扫都得扫一遍,152 MB 的 external 只是一笔记账。元素多、生命周期长的数值流(时序点、坐标、音频帧)换成 typed array,省的不是几个字节,是每次 GC 的停顿。

确认元素种类只需要一行:

node --allow-natives-syntax -e 'const a=[1,2,3]; a[1]=0.5; %DebugPrint(a)'

输出里会写明元素种类是 PACKED_DOUBLE_ELEMENTS。

实操要点

  1. 数值密集的数据用 Float64Array / Int32Array,别用普通数组装数字。
  2. 不要用 null、undefined、空字符串表示空值。它们的槽位是对象引用,会直接把数组降级成指针数组。用 NaN 或约定的哨兵值。
  3. 不要 delete a[i],也不要给短数组赋 a[1000] = x。这会让数组变成 HOLEY(带空洞),访问路径要额外查原型链。
  4. new Array(n) 预分配出来的是 HOLEY_SMI,不是 PACKED。真正等价要 push 或 fill 之后。
  5. 循环体尽量和元素种类对齐:整数数组配整数运算,double 数组配浮点累加,别在一个热循环里来回横跳。

数据来自 Node 24.22.1 / Windows,参数 --expose-gc --max-old-space-size=4096。绝对值随机器变化,比例关系稳定。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。