首页 > 解决方案 > 为什么数组结构没有比 JavaScript 中的结构数组快得多?

问题描述

我一直在阅读有关实体组件系统上下文中的面向数据的编程。显然,使用数组结构可以更有效地利用缓存并显着提高性能。基本上,如果您正在迭代的所有数据都是连续的,则可以利用缓存局部性来大幅提高性能。

因为我将使用 Javascript 工作,所以我想我首先要设计一个小的基准,看看在理想条件下可以提高多少性能。我做得很简单。在第一个测试中,我对遍历结构数组的速度进行了基准测试,在第二个测试中,我对遍历数组结构的速度进行了基准测试。

这是代码:

function randomInt() { return Math.floor(Math.random() * 100) + 1; }
function randomStr() { return Math.random().toString(36).substring(7); }

let samples = 1000;
let count = 10000000;

function benchmarkArrayOfStructs() {
  let AOS = [];

  for (let i = 0; i < count; i++) {
    AOS.push({ health: randomInt(), name: randomStr(), damage: randomInt() });
  }

  let t1 = performance.now();
  
  let sum = 0;

  for (let x = 0; x < samples; x++) {
    for (let i = 0; i < AOS.length; i++) {
      let item = AOS[i];
    
      sum += item.health + item.damage;
    }
  }
    
  console.log(performance.now() - t1);
}

function benchmarkStructOfArrays() {
  let SOA = { health: [], name: [], damage: [] }

  for (let i = 0; i < count; i++) {
    SOA.health.push(randomInt());
    SOA.name.push(randomStr());
    SOA.damage.push(randomInt());
  }

  let t2 = performance.now();
  
  let sum = 0;

  let h = SOA.health;
  let d = SOA.damage;

  for (let x = 0; x < samples; x++) {
    for (let i = 0; i < count; i++) {  
      sum += h[i] + d[i];
    }
  }

  console.log(performance.now() - t2);
}

benchmarkArrayOfStructs();
benchmarkStructOfArrays();

有趣的是,后一种解决方案仅比第一种解决方案快 20% 左右。在我看过的各种谈话中,他们声称这种操作的速度提高了 10 倍。另外,直觉上我觉得后一种解决方案应该更快,但事实并非如此。现在我开始怀疑这种优化是否值得集成到我的项目中,因为它严重降低了人体工程学。我在基准测试中做错了什么,还是这是实际预期的加速?

标签: javascriptarraysperformancebenchmarkingcpu-cache

解决方案


JavaScript 在 JITting 时不会使用 SIMD 自动矢量化。这是 SoA 布局允许的最大优势之一,但您没有利用它。(而且 AFAIK 在 JS 中不容易。)

此外,如果您的代码是在原本空闲的桌面机器上运行的唯一线程,那么您的线程可用的内存带宽比在典型服务器上或在所有内核竞争内存访问的繁忙机器上要多得多。(由于更高的延迟互连,英特尔至强的最大每核 DRAM 内存带宽较低,但在所有内核繁忙的情况下总带宽更高。这是假设您错过了私有 L2 缓存。)因此,您的基准测试可能测试了您有很多多余的情况内存带宽。

如果您的对象更大,您可能还会从 SoA 中看到更多好处。您的 AoS 循环正在从每个数组元素中读取 3 个对象中的 2 个,因此只有一小部分带入缓存的数据被“浪费”了。如果您尝试使用更多循环不使用的字段,您可能会看到更大的优势。


推荐阅读