在 JavaScript 的数组中,一个能提高 4 倍性能优化的方法!
时间:2026-1-10 23:06 作者:独元殇 分类: 前端技术
今天说个反认知的过去发现的经验。
是数组上的,比较使用,知道了用好了,关键时刻能提高 4 倍的速度!
是这样的,我以前做过小项目,需要储存坐标。现在有三种方案:
第一种:[{x,y,z}, {x,y,z}, {x,y,z} .... ]
第二种:x:[n, n, n, n....] y:[n, n, n, n....] z:[n, n, n, n....]
第三种:[x, y, z, x, y, z, x.....]
大家认为哪个更好呢?这问题还真难。
在算法业界,第一种叫 AoS 结构体数组,第二种 SoA 数组结构体,第三种叫 交错数组 。
感觉都还可以。但差距巨大!
当然,方法二 和 方法三 肯定用类型化数组,也就是 Float64Array。
实际上,比你想象中大,但它们的差距有 4 倍之大!肯定有差距,但是不是小差距,而是 4 倍!
最终的最优解是 第二种!
这是一个基准函数,大家可以打开浏览器控制台试一试:
const CONFIG = {
COUNT: 10_000_000, // 1000 万个元素
ITERATIONS: 20, // 平滑噪声干扰的迭代次数
};
function main() {
console.log(`正在生成 ${CONFIG.COUNT.toLocaleString()} 个实体...`);
// 生成 AOS
const aos = new Array(CONFIG.COUNT);
for (let i = 0; i < CONFIG.COUNT; i++) {
aos[i] = { x: Math.random(), y: Math.random(), z: Math.random() };
}
// 生成 SOA
const soa = {
x: new Float64Array(CONFIG.COUNT), // 注意,是小数
y: new Float64Array(CONFIG.COUNT),
z: new Float64Array(CONFIG.COUNT)
};
for (let i = 0; i < CONFIG.COUNT; i++) {
soa.x[i] = Math.random();
soa.y[i] = Math.random();
soa.z[i] = Math.random();
}
// 生成交错数组
const interleaved = new Float64Array(CONFIG.COUNT * 3);
for (let i = 0; i < CONFIG.COUNT; i++) {
const k = i * 3;
interleaved[k] = Math.random();
interleaved[k+1] = Math.random();
interleaved[k+2] = Math.random();
}
console.log(`生成完毕,开始测试 (迭代 ${CONFIG.ITERATIONS} 次)...`);
// 执行测试
const results = {
AoS: measure("AoS (对象数组)", () => {
let sum = 0;
for (let i = 0; i < CONFIG.COUNT; i++) {
sum += aos[i].x + aos[i].y + aos[i].z;
}
return sum;
}),
SoA: measure("SoA (TypedArray)", () => {
let sum = 0;
const { x, y, z } = soa;
for (let i = 0; i < CONFIG.COUNT; i++) {
sum += x[i] + y[i] + z[i];
}
return sum;
}),
Interleaved: measure("交错存储", () => {
let sum = 0;
const len = CONFIG.COUNT * 3;
for (let i = 0; i < len; i += 3) {
sum += interleaved[i] + interleaved[i+1] + interleaved[i+2];
}
return sum;
})
};
// 输出报告
console.table(results);
const baseline = results.AoS.time;
console.log(`SoA 比 AoS 快 ${(baseline / results.SoA.time).toFixed(2)} 倍`);
console.log(`交错存储 比 AoS 快 ${(baseline / results.Interleaved.time).toFixed(2)} 倍`);
}
/**
* 暴力基准测试
*/
function measure(label, fn) {
let dummy = 0;
for (let i = 0; i < 5; i++) dummy += fn(); // 没用,就是让 编译器 开启优化模式
const start = performance.now();
for (let i = 0; i < CONFIG.ITERATIONS; i++) {
dummy += fn(); // 反复求平均数,也就是平滑
}
const totalTime = performance.now() - start; // 计算花的时间
return {
mode: label,
time: Number(totalTime.toFixed(2)),
checksum: dummy
};
}
main();
第一种,肯定不能用类型化数组,那第二种第三种,如果凭直觉,使用类型化数组,无非也就快个 1.5 1.8 倍?!
但实际上,有 4 倍!
原因并不单纯因为 类型数组 快,Chrome V8 的解释器 JIT 优化,不会让第一种【结构体数组】慢特别多的。
如果你不信,你可以试试都使用整数(刚才的 基准测试 用的小数嘛),发现其实也慢不了多少。
如果你工作里,之前用的整数,发现没差别,就使用第一种(毕竟直观、方便)了,那么后续想当然改成小数,就吃亏了。
真正原因是 :第二种方式,数组结构体,里面不包含各种无用的开销,几乎纯净版。
第一种 AoS 结构体数组里,这个过程中,对堆分配非常多(要分配很多对象),然后 GC 跟踪压力会很大!成本很高,因此可能有起码 4 倍的开销。
而且第一种 AoS 的属性访问,肯定没法跟类型化数组里的 直接访问,直接基于索引的内存读写快。第一种,每次访问,都要先查 x ,然后再读索引值,即便 V8 优化再多,也会很慢。
那为什么换成纯整数,却差别不大呢?
原因很简单,一个普普通通的数组,就像第一个那样,即便加上了对象,也是普通的数组。里面如果都存放整数,解释器 V8 会发现,并且把它也转化为类似于类型化数组一样的差不多的性能。
至于第二和第三种,如果是 交错数组,那么会带来额外的索引算术,其实也是一种开销。因此,每项都列一个单独的数组,是最优的!
大家感兴趣,可以回去琢磨一下...... 还是挺实用的。