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.
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.
Share Improve this question asked Nov 4, 2018 at 23:49 Simon SchickSimon Schick 985 bronze badges 1-
Notice that calling
push
N times on an array of initially X elements has a different result from setting the array's length to N. – Bergi Commented Nov 5, 2018 at 20:25
1 Answer
Reset to default 13V8 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 bee 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 plicated -- 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.