«

在 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 会发现,并且把它也转化为类似于类型化数组一样的差不多的性能。

至于第二和第三种,如果是 交错数组,那么会带来额外的索引算术,其实也是一种开销。因此,每项都列一个单独的数组,是最优的!

大家感兴趣,可以回去琢磨一下...... 还是挺实用的。

标签: 原创 性能优化