En Javascript (V8) ¿por qué forEach en una matriz consume mucha más memory que un simple bucle?

Estoy realizando una validation de datos simple en un gran set de datos en Node.js (versión v7.5.0, con una matriz de 15849×12771 inputs). Todo el set de datos está en la memory por ahora, por razones de performance. Por lo tanto, es fundamental para mí networkingucir la cantidad de memory consumida a un mínimo teórico (cada número representa 8 bytes en JS).

Por favor compare las siguientes forms de lograr lo mismo.

con forEach

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

Esto consume toda la memory de mi process de nodos a 4GB +, causando que nunca (hasta que mi paciencia se agote de todos modos) termine el ciclo (supongo que usará una memory de intercambio más lenta).

Y luego la versión idéntica con un típico 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}]`); } } } 

Esto no consume prácticamente memory adicional, lo que hace que la validation se realice en less de un segundo.

¿Este comportamiento es el esperado? Había anticipado que, dado que forEach se cerraban los ámbitos, no habría problemas de uso de memory adicional en comparación con un ciclo for tradicional.

EDITAR: testing independiente

nodo –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}]`); } }); }); }; 

Salida:

 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 

(Desarrollador de V8 aquí.) Esta es una desafortunada consecuencia de cómo Array.forEach se implementa en la antigua línea de ejecución de V8 (código completo + cigüeñal). En resumen, lo que sucede es que, en algunas circunstancias, el uso de forEach en una matriz cambia la representación interna de esa matriz en un formatting mucho less eficiente en memory. (Específicamente: si la matriz solo contenía valores dobles antes, y forEach también se ha utilizado en matrices con elementos de otros types pero no demasiados types diferentes de objects, y el código se ejecuta lo suficientemente caliente como para optimizarlo. Es bastante complicado 😉 )

Con la nueva canalización de ejecución (actualmente detrás del indicador --future , se activará de manera pnetworkingeterminada pronto), ya no veo este consumo de memory adicional.

(Dicho esto, los loops clásicos tienden a tener una pequeña ventaja de performance respecto de cada forEach , solo porque hay less actividad debajo del capó (según la especificación ES). En muchas cargas de trabajo reales, la diferencia es demasiado pequeña para importar, pero en microbenchmarks es a menudo visible. Podríamos optimizar más en el futuro los gastos generales de cada uno, pero en los casos en que usted sabe que cada ciclo de la CPU es importante, recomiendo usar el método antiguo for (var i = 0; i < array.length; i++) loops.)