为什么在Javascript (V8)中,对于一个数组而言,forEach循环所消耗的内存比简单的for循环要多?

8
我正在对一个大数据集(在 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}]`);
      }
    }
  }

这几乎不占用额外的内存,使验证在不到一秒钟的时间内完成。

这种行为是否符合预期?我原以为由于 forEach 具有闭合作用域,与传统的 for 循环相比,不会出现额外的内存使用问题。

编辑:独立测试

node --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行中删除该引用,您的内存消耗会如何? - David Thomas
1
那看起来很奇怪... 你能否使用预定义的数据集和一个isFinite(虚拟)函数使其可重现,以便可以独立运行? - CFrei
“无额外内存”在硬数字方面是什么意思? - Bergi
@CFrei 我已经更新了问题,并提供了一个独立的可运行测试。@Bergi 请查看测试,其中 for 循环使用了额外的 0.3 兆字节,而 forEach 使用了数千兆字节。 - Tom
@DavidThomas 感谢您的建议,我尝试了一下,但好像并没有影响到内存消耗。我在问题中添加了一个独立测试,这样您就可以自己尝试一下。 - Tom
2
这个问题的回答可能会帮到你:https://dev59.com/Z4jca4cB1Zd3GeqPrROw。似乎`for ... inObject.keys.forEach`被认为是内存占用量大的函数。我甚至在node v7.6.0上尝试,但仍然没有足够的内存。我想V8未来可能会重写它们的实现方式,以防止将整个数组加载到内存中,而只是通过索引进行迭代。 - David Thomas
1个回答

10
2022更新:此问题及答案已过时。 下面原始答案中提到的“新执行管道”已经启用了几年。
原帖如下(如果您仍在运行2017年的Node):
(V8开发人员在此)。这是使用V8旧的执行管道(full codegen + Crankshaft)实现Array.forEach的不幸后果。简而言之,发生的情况是,在某些情况下,在数组上使用forEach会将该数组的内部表示更改为一个更不占用内存的格式。(具体来说:如果数组之前只包含双精度值,并且forEach还用于具有其他类型元素但不太多不同种类对象的数组,并且代码运行足够热以获得优化。它相当复杂;-))
通过新的执行管道(目前在--future标志后面,默认情况下将很快打开),我不再看到这种额外的内存消耗。
(也就是说,经典的for循环确实比forEach具有小的性能优势,只是因为在ES规范中没有太多的事情要做。在许多真实的工作负载中,差异太小而无关紧要,但在微基准测试中通常是可见的。我们可能能够在未来优化掉更多的forEach开销,但在您知道每个CPU周期很重要的情况下,我建议使用普通的for(var i = 0; i < array.length; i++)循环。)

1
有趣!我该如何在 node.js 中启用 --future?运行 node --v8-options 时我没有看到它。 - Tom
1
请查看 https://medium.com/@bmeurer/help-us-test-the-future-of-node-js-6079900566f#.t38ycyhzp。 - jmrk
@jmrk 这个问题今天还有意义吗? - Elec
1
@Elec No。新的执行管道已经启用多年。(您也可以非常轻松地验证自己:只需运行给定的测试用例。) - jmrk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接