Search code examples
javascriptarraysperformancev8

Array resize performance, setting length property vs. repeated pushing


So I was benchmarking the following code, trying to figure out which would be more performant:

'use strict';

function addSetToArrayA(array, set) {
  for (const v of set) {
    array.push(v);
  }
}
function addSetToArrayB(array, set) {
  const origLength = array.length;
  const newLength = array.length + set.size;
  array.length = newLength;
  array[newLength - 1] = 0;
  let i = origLength;
  for (const v of set) {
    array[i++] = v;
  }
}

const set = new Set([1, 2, 3, 4, 5, 6]);

console.time('addSetToArrayA');
for (let i = 0;i<0xffffff;++i) {
  const base = [1, 2, 3, 4, 5, 6];
  addSetToArrayA(base, set);
}
console.timeEnd('addSetToArrayA');

console.time('addSetToArrayB');
for (let i = 0;i<0xffffff;++i) {
  const base = [1, 2, 3, 4, 5, 6];
  addSetToArrayB(base, set);
}
console.timeEnd('addSetToArrayB');

The result surprised me a little:

addSetToArrayA: 728.773ms
addSetToArrayB: 3296.437ms

So I did another benchmark:

'use strict';

const iters = 0xfffff;

console.time('32 push');
for (let i = 0;i<iters;++i) {
  const base = [1, 2, 3, 4, 5, 6];
  for (let k = 0;k<32;++k) {
    base.push(undefined);
  }
}
console.timeEnd('32 push');

console.time('32 length');
for (let i = 0;i<iters;++i) {
  const base = [1, 2, 3, 4, 5, 6];
  base.length = 32;
}
console.timeEnd('32 length');

console.time('64 push');
for (let i = 0;i<iters;++i) {
  const base = [1, 2, 3, 4, 5, 6];
  for (let k = 0;k<64;++k) {
    base.push(undefined);
  }
}
console.timeEnd('64 push');

console.time('64 length');
for (let i = 0;i<iters;++i) {
  const base = [1, 2, 3, 4, 5, 6];
  base.length = 64;
}
console.timeEnd('64 length');

console.time('128 push');
for (let i = 0;i<iters;++i) {
  const base = [1, 2, 3, 4, 5, 6];
  for (let k = 0;k<128;++k) {
    base.push(undefined);
  }
}
console.timeEnd('128 push');

console.time('128 length');
for (let i = 0;i<iters;++i) {
  const base = [1, 2, 3, 4, 5, 6];
  base.length = 128;
}
console.timeEnd('128 length');

The results were in line with what I experienced previously:

32 push: 132.061ms
32 length: 180.745ms
64 push: 284.575ms
64 length: 212.465ms
128 push: 586.747ms
128 length: 268.689ms

Eg. for relatively small arrays re-sizing via .length is slower than repeatedly using .push(), while for larger arrays it's faster (as expected).

Is V8 performing different types of array resizing when using .length = ... vs. .push(...)? Is this related to how V8 handles sparse arrays?

Running on [email protected] with similar results in chrome.


Solution

  • V8 developer here. The short answer is that .push() is super optimized, whereas writing to .length is a fairly slow operation (partially because of what the JavaScript spec says it must do, and partially because we haven't optimized it quite as much -- but even if we did, it wouldn't become as fast as .push() for a few elements).

    In fact, you'll notice a similar difference between writing to .length to shorten an array and calling .pop() a couple of times.

    Personally I think this is not a bad state to be in: your .push based code is concise, intuitive, and readable. The .length based alternative looks like an attempt to squeeze out a bit of extra performance at the cost of making the code uglier and more complicated -- isn't it nice that that does not help? Write the code you want to write, let the engine worry about making it fast! :-)

    Is V8 performing different types of array resizing when using .length = ... vs. .push(...)?

    Writing to .length and calling .push(...) are very different operations (the latter is pretty straightforward, the former must perform a bunch of checks), so yeah, what V8 does under the hood is necessarily different. The resizing itself, if/once it happens, is the same.

    Is this related to how V8 handles sparse arrays?

    Not in your example. In general, writing to .length must check whether the array should transition to sparse mode, but that check itself is pretty fast.