.forEach vs Object.keys().forEach performance on s

2020-03-26 08:26发布

问题:

Tell me if I'm wrong: array.forEach(callbackFunction) is suited for sparse arrays. It executes callbackFunction not for each index between zero and the array length, but only for the keys which are actually in the array. And (tell me if I'm wrong) those keys are exactly what Object.keys(array) will give me. Hence (tell me why I'm wrong) it shouldn't make a difference if the .forEach method is called on array itself or on Object.keys(array). So, why on earth is there this performance difference - as if, in one case, a giant pointless loop from zero to length would be executed, but not in the other case.

Snippet showing performance difference:

function doNothing(){}
CONSOLE = document.getElementById('console');

arr = [];
arr[49888999] = 42;

start = performance.now();
arr.forEach(doNothing);
duration1 = performance.now() - start;

start = performance.now();
Object.keys(arr).forEach(doNothing);
duration2 = performance.now() - start;

CONSOLE.textContent = [duration1, duration2].join('\n');
<pre id='console'></pre>

Snippet showing that the callback function IS CALLED ONLY ONCE in BOTH cases

console1 = document.getElementById('console1');
console2 = document.getElementById('console2');
function doNothingVerbose1(){
  console1.textContent = 1 + (+console1.textContent);
}
function doNothingVerbose2(){
  console2.textContent = 1 + (+console2.textContent);
}

arr = [];
arr[49888999] = 42;

start = performance.now();
arr.forEach(doNothingVerbose1);
duration1 = performance.now() - start;

start = performance.now();
Object.keys(arr).forEach(doNothingVerbose2);
duration2 = performance.now() - start;

console.log(duration1, duration2);
~~~~~ 1 ~~~~~
<pre id='console1'>0</pre>
~~~~~ 2 ~~~~~
<pre id='console2'>0</pre>

UPDATE

I just did a test to find out whether or not the above arr=[];arr[49888999]=42; is an actual sparse array, i.e. has much less memory footprint compared to doing arr=new Array(49889000). And yes, that is the case. Doing this hundreds of times in a loop, the sparse version takes a couple of seconds but doesn't crash, but the new Array(50 million) version crashes the fiddle. So if it's not stored as a 'normal C++ array' in the engine then the engine must "have" Object.keys of the array, so why doesn't the engine make efficient use of it? I might have a too simplistic view of what a JS engine has to do; is it wrong to say that the engine must "have" Object.keys because it "has" a sparse array implementation backing our variable arr in some fashion? Maybe someone actually working on a browser/JS engine can shed some light here.

回答1:

as if because, in one case, a giant pointless loop from zero to length would be is executed, but not in the other case.


According to the ECMA documentation:

  1. The .forEach method will loop through all array elements by its .length property.
  2. The callback passed to .forEach will only be invoked if an element is not empty.

To demonstrate this you can simply do:

function doNothing(){}
let perf;


console.log('Array with 50 million length and 1 non-empty element:');
const a = [];
a[49999999] = 'a';
console.log('a.length:', a.length);

perf = performance.now();
a.forEach(doNothing);
console.log('a:', performance.now() - perf);
console.log('');


console.log('Array with 0 length:');
const b = [];
b.foo = 'a';
console.log('b.length:', b.length);

perf = performance.now();
b.forEach(doNothing);
console.log('b:', performance.now() - perf);
console.log('');


console.log('Array with 50 million length and 0 non-empty element:');
const c = [];
c.length = 50000000;
console.log('c.length:', c.length);

perf = performance.now();
c.forEach(doNothing);
console.log('c:', performance.now() - perf);



回答2:

not answering the 'why' question here - except that I think that Kaiido is most probably speculating right on target (see comments below question; also see jmrk's comment under Yong's answer, with which I agree)

Maybe a more interesting question is how to deal with the phenomenon in practice. If, for example, I have to deal with a 'sparse Array' as in "2 items of product 51472 and 1 item of product 81369", I'll use an object ({}) with keys 51472 and 81369, and not an array ([]).

Making it an array just because all keys happen to be non-negative integers is a bad idea the worst idea of the last 10 thousand years - because you then have .forEach, which is a FALSE FRIEND

2 related questions:

Why are we allowed to create sparse arrays in JavaScript

What use cases are there in JavaScript for Sparse Arrays?