Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is comparing an array and a number so slow in JavaScript? What is it doing?

I accidentally compared a large array and a number with <, and JavaScript locked up for over 5 seconds. What is the expected behavior of this comparison? Is it iterating over the whole array? MDN didn't clarify the situation.

As a concrete example, this code snippet takes over 5 seconds to print done:

var m = [];
m[268435461] = -1;
console.log('start');
if (m < 0) { }
console.log('done');
like image 840
Ken Shirriff Avatar asked Dec 07 '25 19:12

Ken Shirriff


1 Answers

Javascript "arrays" (those with Array prototype, not typed arrays), are just objects, therefore this

var m = [];
m[268435461] = -1;

is exactly the same as

var m = {
    "268435461": -1
}

except that in the first case, m has the Array prototype and a special length property.

However, methods defined in Array.prototype (like forEach or join) are trying to hide that fact and "emulate" sequential arrays, as they exist in other languages. When iterating their "this" array, these methods take its length property, increase the loop counter from 0 upto length-1 and do something with the value under the key String(i) (or undefined if there's no such key)

// built-in js array iteration algorithm

for (let i = 0; i < this.length - 1; i++) {
     if (this.hasOwnProperty(String(i))
         do_something_with(this[String(i)])
     else
         do_something_with(undefined)

Now, length of an array is not a number of elements in it, as the name might suggest, but rather the max numeric value of its keys + 1, so in your case, length will be 268435462 (check it!)

When you do m < 0, that is, compare a non-number to a number, JS converts them both to strings, and Array.toString invokes Array.join, which, in turn, uses the above loop to convert elements to strings and insert a comma in between:

// built-in js Array.join algorithm

target = '';

for (let i = 0; i < this.length - 1; i++) {
    let element = this[String(i)]

    if(element !== undefined)
        target += element.toString()

    target += ','
}

Illustration:

m  = [];
m[50] = 1;
console.log(m.join())

This involves lots of memory allocations, and that's what is causing the delay.

(After some more testing, the allocation are not the deciding factor here, "hollow" loops will cause the same slowdown:

console.time('small-init')
var m = [];
m[1] = -1;
console.timeEnd('small-init')

console.time('small-loop')
m.forEach(x => null)
console.timeEnd('small-loop')

console.time('big-init')
var m = [];
m[1e8] = -1;
console.timeEnd('big-init')

console.time('big-loop')
m.forEach(x => null);
console.timeEnd('big-loop')

That being said, I don't think modern JS engines are that silly, and implement iterations exactly as described above. They do have array-specific optimizations in place, but these optimizations are targeted at "good" sequential arrays, and not at bizarre edge cases like this. Bottom line: don't do that!

like image 172
georg Avatar answered Dec 09 '25 09:12

georg



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!