【问题标题】:In Javascript (V8) why does forEach on an array consume much more memory than a simple for loop?在 Javascript (V8) 中,为什么数组上的 forEach 比简单的 for 循环消耗更多的内存?
【发布时间】:2017-07-17 00:22:23
【问题描述】:

我正在对 Node.js(版本 v7.5.0,包含 15849x12771 个条目的矩阵)中的大量数据执行一些简单的数据验证。出于性能原因,现在整个数据集都在内存中。因此,将消耗的内存量减少到理论上的最小值(每个数字在 JS 中代表 8 个字节)对我来说至关重要。

请比较以下实现相同目标的方法。

forEach

  regressData.forEach((yxa, yxaIndex) => {
    yxa.forEach((yx, yxIndex) => {
      if (!_.isFinite(yx)) {
        throw new Error(`non-finite entry at [${yxaIndex}, ${yxIndex}]`);
      }
    });
  });

这会消耗我所有节点进程的 4GB+ 内存,导致它永远不会(直到我的耐心耗尽)完成循环(我猜它会使用较慢的交换内存)。

然后是与典型for相同的版本:

  for (var yxai = 0, yxal = regressData.length; yxai < yxal; yxai++) {
    const yx = regressData[yxai];
    for (var yxi = 0, yxl = yx.length; yxi < yxl; yxi++) {
      if (!_.isFinite(yx[yxi])) {
        throw new Error(`non-finite entry at [${yxai}, ${yxi}]`);
      }
    }
  }

这几乎不会消耗额外的内存,从而在不到一秒的时间内完成验证。

这种行为是否符合预期?我曾预料到,由于 forEachs 具有封闭范围,因此与传统的 for 循环相比,不会出现额外内存使用的问题。

编辑:独立测试

节点 --expose-gc test_foreach.js

if (!gc) throw new Error('please run node like node --expose-gc test_foreach.js');

const _ = require('lodash');

// prepare data to work with

const x = 15849;
const y = 12771;

let regressData = new Array(x);
for (var i = 0; i < x; i++) {
  regressData[i] = new Array(y);
  for (var j = 0; j < y; j++) {
    regressData[i][j] = _.random(true);
  }
}

// for loop
gc();
const mb_pre_for = _.round(process.memoryUsage().heapUsed / 1024 / 1024, 2);
console.log(`memory consumption before for loop ${mb_pre_for} megabyte`);
validateFor(regressData);
gc();
const mb_post_for = _.round(process.memoryUsage().heapUsed / 1024 / 1024, 2);
const mb_for = _.round(mb_post_for - mb_pre_for, 2);
console.log(`memory consumption by for loop ${mb_for} megabyte`);

// for each loop
gc();
const mb_pre_foreach = _.round(process.memoryUsage().heapUsed / 1024 / 1024, 2);
console.log(`memory consumption before foreach loop ${mb_pre_foreach} megabyte`);
validateForEach(regressData);
gc();
const mb_post_foreach = _.round(process.memoryUsage().heapUsed / 1024 / 1024, 2);
const mb_foreach = _.round(mb_post_foreach - mb_pre_foreach, 2);
console.log(`memory consumption by foreach loop ${mb_foreach} megabyte`);

function validateFor(regressData) {
  for (var yxai = 0, yxal = regressData.length; yxai < yxal; yxai++) {
    const yx = regressData[yxai];
    for (var yxi = 0, yxl = yx.length; yxi < yxl; yxi++) {
      if (!_.isFinite(yx[yxi])) {
        throw new Error(`non-finite entry at [${yxai}, ${yxi}]`);
      }
    }
  }
};

function validateForEach(regressData) {
  regressData.forEach((yxa, yxaIndex) => {
    yxa.forEach((yx, yxIndex) => {
      if (!_.isFinite(yx)) {
        throw new Error(`non-finite entry at [${yxaIndex}, ${yxIndex}]`);
      }
    });
  });
};

输出:

toms-mbp-2:mem_test tommedema$ node --expose-gc test_foreach.js
memory consumption before for loop 1549.31 megabyte
memory consumption by for loop 0.31 megabyte
memory consumption before foreach loop 1549.66 megabyte
memory consumption by foreach loop 3087.9 megabyte

【问题讨论】:

  • 在您的第一个示例中,您在内部循环中引用 yxaIndex。如果您在 new Error 行中删除该引用,您的内存消耗情况如何。
  • 这似乎很奇怪......你能用一个预定义的数据集和一个 isFinite(虚拟)函数让它可重现,这样就可以独立运行了吗?
  • “没有额外的内存”,你的硬数字是什么意思?
  • @CFrei 我已经用独立的可运行测试更新了这个问题。 @Bergi 看测试,for 循环使用了 0.3 兆字节,而forEach 使用了几千兆字节。
  • this question 中的答案可能会对您有所帮助。似乎for ... inObject.keys.forEach 是众所周知的记忆猪。我什至尝试使用节点 v7.6.0,但它仍然内存不足。我想 V8 将来可能会重写他们对这些函数的实现,以阻止他们将完整的数组加载到内存中,而不是仅仅用索引遍历它们。

标签: javascript arrays memory optimization v8


【解决方案1】:

(这里是 V8 开发人员。)这是在 V8 的旧执行管道(完整代码生成 + 曲轴)中如何实现 Array.forEach 的不幸结果。简而言之,在某些情况下,在数组上使用 forEach 会将该数组的内部表示更改为内存效率低得多的格式。 (特别是:如果数组之前只包含双精度值,并且forEach 也被用于具有其他类型元素但没有太多不同类型对象的数组,并且代码运行得足够热以进行优化。这相当复杂; -))

使用新的执行管道(目前在--future 标志后面,很快就会默认打开),我不再看到这种额外的内存消耗。

(也就是说,经典的 for 循环确实比 forEach 具有较小的性能优势,只是因为底层发生的事情更少(根据 ES 规范)。在许多实际工作负载中,差异太小很重要,但在微基准测试中它通常是可见的。将来我们可能能够优化更多 forEach 的开销,但如果你知道每个 CPU 周期都很重要,我建议使用普通的旧 for (var i = 0; i &lt; array.length; i++) 循环.)

【讨论】:

  • 有趣!如何使用 node.js 启用--future?我运行时看不到它node --v8-options
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-04-22
  • 2015-01-16
  • 2020-10-14
  • 1970-01-01
  • 2021-12-09
  • 1970-01-01
相关资源
最近更新 更多