公司动态
说 TypedArray 计算快 4 倍?实测:连续数组只慢 1 倍,类型一乱却慢 7.6 倍
“数值计算密集换成 TypedArray 能快好几倍”——这话几乎成了共识。可当你真把一个千万级的普通数组改成Float64Array跑分下来却发现求和只快了一点点反倒是“顺手转一下类型”这件事悄悄吃掉了几十毫秒甚至因为一个字符串混进数组就让循环慢了七倍多。本文用 Node 22 上的真实跑分把“TypedArray 一定更快”这条经验拆解开给出能照着用的决策边界。背景为什么这件事值得写凡是碰过 Canvas 像素、Web Audio 采样、二进制协议、或者 Tensor 类库的开发者都绕不开 TypedArray。社区里流传的说法很一致普通数组“每个元素是一个 JS 值有额外开销”TypedArray“紧凑二进制、直接操作内存数值计算能快 2–10 倍”。这话没错但不完整。它把三件不同的事混在了一起稳态计算速度、内存占用、构造/类型转换成本。我见过不少代码为了“提速”在热路径里反复new Float64Array(plainArr)做一次性拷贝结果拷贝本身比后面那点计算还贵也见过数组里不小心混进一个字符串整段数值循环肉眼可见地卡住却没人想到是“元素类型退化”在作怪。所以问题不是“用不用 TypedArray”而是在哪些具体场景下它真的值哪些场景下它只是徒增复杂度。下面用跑分回答。解剖V8 里两种数组到底怎么存要理解跑分得先看内存布局。V8 对普通数组有二十多种“元素类型elements kind”其中跟数值最相关的是PACKED_DOUBLE_ELEMENTS当一个数组只装数字、且连续无空洞时V8 会把双精度浮点直接内联在后备存储里每个元素 8 字节没有对象包装。这恰恰是“普通数组求和并不慢”的根本原因——它本来就已经是连续内存了。TypedArray 则是另一种思路在ArrayBuffer上套一个固定宽度的视图元素宽度在创建时就锁死Float64Array恒为 8 字节、Uint8Array恒为 1 字节布局由引擎保证连续、同构、不可变长。两者的关键差别不在“是否连续”而在两点宽度是否可压缩TypedArray 能选 1/2/4 字节类型以及类型是否会被污染。普通数组是“柔性”的——一旦塞进一个字符串、出现空洞、或delete一个元素元素类型会从PACKED_DOUBLE永久退化到PACKED_ELEMENTS每个元素变成带标签的指针甚至DICTIONARY模式且退化不可逆。图1左侧普通数组在纯数字且连续时是 PACKED_DOUBLE每元素 8 字节内联一旦混类型/空洞即永久退化右侧 TypedArray 在 ArrayBuffer 上套固定宽度视图宽度可压缩到 1/2/4 字节。实证一次真实跑分环境Node v22.22.2--expose-gc每种配置 1 次 warmup 11 轮取中位。数据量为 1e6 与 1e7 的随机双精度数组普通数组保证 PACKED_DOUBLE。测三个计算模式 一次构造拷贝 一次类型退化# 复现在 2026-08-27-noon 目录下 node --expose-gc bench.js # 输出写入 bench-results.json场景N1e7普通数组Float64Array普通慢几倍只读求和 sum4.02 ms3.96 ms1.02×缩放拷贝读写新缓冲21.11 ms12.64 ms1.67×原地写回 in-place4.56 ms4.40 ms1.04×混一个字符串后求和30.59 ms—7.61×相对 packed构造new Float64Array(plain)—45.56 ms一次性 O(n) 拷贝—N1e6 时比例略大sum 1.30×、缩放拷贝 2.91×、原地 1.13×、混字符串 5.65×。图2只有“缩放拷贝”需要分配新缓冲并写入差距明显1.7–2.9×只读求和与原地写回差距极小1.0–1.3×。差异来自写新缓冲时的类型处理而非“TypedArray 天生快 4 倍”。数字透露出的事实只读求和几乎无差。普通数组已经是 PACKED_DOUBLE 连续内存热点循环被 V8 优化得和 TypedArray 一个量级。所谓“快 4 倍”在稳态读取上并不成立。差距在“写新缓冲”。缩放拷贝里普通数组要新建一个数组并逐个写值TypedArray 的定宽写入更顺拉开到 1.7–2.9×。注意这仍是“写新缓冲”的功劳不是读取。真正的大坑是类型退化。往普通数组末尾塞一个字符串求和从 4.02 ms 暴涨到 30.59 ms7.6×且这个退化对这块内存永久生效。TypedArray 因为创建时锁死类型根本不存在这条退化路径。构造是有代价的。把普通数组拷成Float64Array本身要一次 O(n) 遍历1e7 约 45 ms。若算法只跑一遍、拷贝却每次都做拷贝成本会吃掉全部“提速”。落地场景Canvas 像素处理最该上 TypedArray 的地方是数据天然就是二进制、且要反复原地多趟处理的场景典型就是 Canvas。getImageData().data返回的就是Uint8ClampedArrayRGBA 各 1 字节你可以再套一个Uint32Array视图一次循环处理整像素避免逐通道寻址// 灰度化对 1e7 级像素量的图像原地多趟处理收益明显 const img ctx.getImageData(0, 0, w, h); const buf32 new Uint32Array(img.data.buffer); // 每元素 一个 RGBA 像素 for (let i 0; i buf32.length; i) { const p buf32[i]; const r p 0xff, g (p 8) 0xff, b (p 16) 0xff; const y (r * 0.299 g * 0.587 b * 0.114) | 0; // 亮度 buf32[i] (p 0xff000000) | (y 16) | (y 8) | y; // 写回同缓冲零拷贝 } ctx.putImageData(img, 0, 0);这里 TypedArray 的价值不在“计算快几倍”而在零拷贝地复用底层ArrayBuffer、用最窄宽度1 字节吃下海量像素、并能与 Web Worker 共享缓冲——这正是普通数组给不了的。图3普通数组在 PACKED_DOUBLE 下求和 4.02 ms仅因混入一个字符串退化到 30.59 ms7.6×。TypedArray 因类型锁死没有这条退化路径——这是它最被低估的“稳定性”价值。局限哪些事没解决内存数字要分情况看。TypedArray 的字节数是精确的.byteLengthFloat64Array恒 8 字节/元素Uint8Array1 字节普通数组在 PACKED_DOUBLE 下经 RSS 实测约 8–9.7 字节/元素两者对双精度几乎持平。TypedArray 真正的省内存优势出现在你能用更窄类型时1e7 个 0–255 的字节Uint8Array用 10 MB普通数组约 80 MB省约 8 倍。本测在 Node 跑浏览器结论方向一致但绝对值不同。V8 的元素类型机制两家共用退化陷阱同样存在但 JIT、GC、主线程调度会让真实页面数字有抖动像素处理还要算上getImageData/putImageData本身的 IO。Float32 有精度代价。需要小数又想省内存时Float32Array只有 4 字节但会丢精度别在金额、坐标累计上无脑用。不是所有循环都该改。一次性求和、且数据本就是普通数组时转类型的一次性拷贝可能比省下的计算还贵——先量再改。结论与下一步一句话TypedArray 不是“计算快 4 倍”的银弹而是“内存可压缩 类型不退化的稳定容器”。决策边界很清楚——用 TypedArray要和普通数组/Worker/网络二进制互操作、能用更窄类型省内存Int8/Uint8/Float32/Int32、或要在同一缓冲上反复原地多趟处理像素、采样、协议解析。用普通数组数据本就是它、只跑一遍数值求和、且能保证纯数字连续别混类型、别留空洞、别delete。最该警惕的反而不是“没用 TypedArray”而是在热路径里反复构造拷贝以及让普通数组发生类型退化——前者白忙后者慢七倍且不可逆。开源地址矩阵门户https://github.com/wangzifan396-wzf/WB单文件工具聚合器https://github.com/wangzifan396-wzf/nano-workbenchGitHub 组织主页https://github.com/wangzifan396-wzf