我正在对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
答案 0 :(得分:6)
(V8开发人员在这里。)这是如何在V8的旧执行管道(完全代码生成器+ Crankshaft)中实现Array.forEach
的不幸结果。简而言之,在某些情况下,在数组上使用forEach
会将该数组的内部表示更改为内存效率更低的格式。 (具体来说:如果数组之前只包含双值,并且forEach
也用于具有其他类型元素但没有太多不同类型对象的数组,并且代码运行得足够热以进行优化。它&# 39;相当复杂;-))
使用新的执行管道(当前位于--future
标志后面,默认情况下将很快打开),我不再看到这种额外的内存消耗。
(也就是说,经典的for
循环确实比forEach
具有更小的性能优势,只是因为在引擎盖下(根据ES规范)的影响较小。真正的工作负载,差异太小而不重要,但在微基准测试中它常常是可见的。我们未来可能能够优化forEach
的更多开销,但是在这种情况下你知道每个CPU周期都很重要,我建议使用普通的旧for (var i = 0; i < array.length; i++)
循环。)