【问题标题】:Why is struct of arrays not vastly faster than array of structs in Javascript?为什么数组的结构没有比Javascript中的结构数组快得多?
【发布时间】:2021-04-28 11:38:09
【问题描述】:

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

因为我将使用 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 倍。另外,直觉上我觉得后一种解决方案应该快得多,但事实并非如此。现在我开始怀疑这种优化是否值得集成到我的项目中,因为它严重降低了人体工程学。我是否在基准测试中做错了什么,或者这是实际预期的加速?

【问题讨论】:

  • 综合基准测试很少能很好地指导您的真实代码在真实情况下会发生什么,JavaScript 引擎会根据代码的使用方式应用不同的优化策略(或根本不应用任何优化策略) .上面的基准非常简单(你会想要做很多数据检索测试,而不仅仅是一个),但这不是主要问题。主要问题是它首先是一个综合基准。
  • 我知道一般的基准测试的陷阱,但这似乎很简单,可以解释。您对如何改进基准有什么建议吗?
  • 正如我所说,您至少要重复测试(在测试中,而不是单独运行)- 很多。 (例如,10,000 多次。)但是您的项目当前代码是否存在严重的性能问题?
  • 我将尝试修改代码示例来做到这一点。也没有当前的代码可以说话。我正在尝试在 Javascript 中为一个项目设计一个最小的实体组件系统,并且我正在使用它来确定使用数组结构与结构数组来实现它是否值得。后者明显更符合人体工程学,所以我更愿意这样做,但如果前者像其他人声称的那样高性能,我可能不得不牺牲人体工程学。我正在尝试确定它是否值得。
  • 我更新了代码以考虑样本,但仍然是相同的百分比差异。无论是 1 还是 10,000 个样本,后者都快 20%。

标签: javascript arrays performance benchmarking cpu-cache


【解决方案1】:

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

另外,如果您的代码是运行在其他空闲的桌面机器上的唯一线程,那么您的线程可用的内存带宽比在典型服务器上或在繁忙机器上多得多所有核心竞争内存访问。 (Intel Xeons have lower max per-core DRAM memory bandwidth 由于更高的延迟互连,但更高的聚合带宽,所有内​​核都忙。这是假设您错过了私有 L2 缓存。)因此,您的基准测试可能测试了您有大量多余内存带宽的情况。

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

【讨论】:

    【解决方案2】:

    在 Javascript 中,不能保证 Array 是一个真正的数组 - 据您所知,引擎可能会在整个内存中存储元素,从而使缓存局部性无效。要强制引擎将数组存储在连续内存中,您必须使用TypedArray

    TypedArrays 本质上也比常规数组快,因为 JS 引擎不必对使用模式进行任何猜测。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-09-28
      • 2015-09-11
      • 2019-02-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-06-16
      相关资源
      最近更新 更多