在Javascript(V8)中,为什么数组上的forEach比简单的for循环消耗更多的内存?

时间:2017-02-26 11:35:19

标签: javascript arrays memory optimization v8

我正在对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

1 个答案:

答案 0 :(得分:6)

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

使用新的执行管道(当前位于--future标志后面,默认情况下将很快打开),我不再看到这种额外的内存消耗。

(也就是说,经典的for循环确实比forEach具有更小的性能优势,只是因为在引擎盖下(根据ES规范)的影响较小。真正的工作负载,差异太小而不重要,但在微基准测试中它常常是可见的。我们未来可能能够优化forEach的更多开销,但是在这种情况下你知道每个CPU周期都很重要,我建议使用普通的旧for (var i = 0; i < array.length; i++)循环。)