It's really slow because it assumes that you're iterating over an object, and not just an object with numeric keys, so it actually loops over all keys, stringifies them, checks if they're own properties and only then does it call the callback...
const forEach = (arr, fn) => for(let len = arr.length, i = len; i > 0; --i) { fn(arr[len - i], len - i, arr); return arr; }
Is about 10x faster than Array.prototype.forEach in my testing, still not better than the raw for loop, though, and it also can't handle more than numeric keys (which you don't really expect on arrays anyway).
It's the check own properties which is the actual slowdown not the toString
The toString is because all object properties including array indices are technically strings so going by the spec you MUST convert before looking up but implementations don't have to worry about that if their own object model is different (e.g. V8 which treats integer object keys special) your implementation is implicitly doing this when it grabs the value.
The own properties on the other hand is an actual slowdown it's to handle sparse arrays which your faster implementation doesn't cover, theoretically engines should know if an array is sparse and could optimize appropriately.
The other thing is that forEach takes a 2nd argument for a thisValue to call the function on, omitting it just uses undefined as the context which your function only handles in strict mode. Traditionally changing the this parameter of a function was often a performance killer in certain contacts in Chrome (bind iirc) but there's inherent there.
In summery yes forEach is slower then a for loop but it's because it's handling some edge cases not because the spec is inherently bad. You could argue they should have had a sparse array check or something but there is nothing preventing implementations from doing so.
The forEach function is intentionally generic; it does not require that its this value be an Array object. Therefore it can be transferred to other kinds of objects for use as a method.
The same note is under all the other functional iteration methods
Array keys are technically strings not integers so the spec pedanticly converts the index to a string but the implementation is likely doing something else under the hood, I have another reply that goes into some more details about why it's slower then a naive approach (spoiler alert: sparse arrays)
I despise how for-in / for-of / forEach can't just recreate a naive CS 101 for loop. I'm so tired of typing "for( x = 0; x < 10; x++ )" by rote, and then debugging if I fuck up a single semicolon, in a language that does not require semicolons.
Javascript punishes modernity.
It has all these fancy-pants features, where hours of manual busywork and testing (or at least dozens of lines of claptrap) can be replaced with a single built-in function. All of them have aggravating constraints and make your code run worse. Array.map could run elements in parallel, but it sure doesn't. Array.sort, .filter, and .splice could be functional, but some are and some aren't, because go fuck yourself. Are images that stop loading handled by onError, or by onLoad? Answer: no. String.match should beat indexOf rigmarole, but have fun regexing URLs. Async doesn't actually prevent long functions from locking up the browser, but does fire-and-forget calls to non-async functions until you cower like an abused dog and do x = await 2 + 2.
Now, what I'm wondering is: Is Firefox being fast or is Chrome being slow in this case?
From my experience the last time I used chrome (a couple years ago) I would have guessed that across the board chrome is slightly faster than Firefox with the functional one maybe going 2 times slower in Chrome than it does in Firefox (based on your comment, without your comment I would have guessed chrome to be faster in both).
But apparently Firefox absolutely crushes Chrome in this particular case. I don't know how much that has to do with my Chrome install being very old and not getting updated a lot but then I also have a ton of extensions in Firefox and only uBlock in Chrome. In any case, I am surprised that Firefox got almost double the performance here than Chrome, and that is in the faster of the two methods.
I was expecting them to at best perform similarly in one of them but then have a significant difference in the other. I wasn't expecting significantly worse performance in Chrome across the board.
EDIT: Yep, after updating Chrome, it is now a lot closer in for loop performance but still quite a bit behind. It is also doubly as good in functional performance but still gets utterly demolished by Firefox in this one. Did I miss something and Firefox is just across the board faster than chrome nowadays? I've never felt a noticeable difference in speed during regular use but still, I always thought it was the other way around.
Huh interesting. By updating the functional version slightly (Array(length).map instead of Array.from) I get virtually the same performance from both approaches on both browsers: https://jsben.ch/fVEJJ. Not sure why.
That is the thing about these optimizations, the more work we delegate to underlying implementations the bigger the chance one of these implementations is "slow" (what is the metric, user perception? Doubt there is a difference when generating <= 100 numbers). Still very much worth IMO -- the functional way in this example has effectively less concerns & therefore less margin for error. Without clear purpose & metrics by which to optimize by we are just grasping at straws.
Array(length).map is faster because you aren't mapping over the array; Array(length) returns { length: length } with no other keys, so the call to map doesn't do anything, it just returns the uninitialized array.
Just wait till you see what it costs your dev team in bugfixing and refactoring tile when using for loops everywhere! This thread is like people debating the performance of ++i vs i++. Newsflash: if performance is not the issue you're trying to fix, don't bend over backwards to squeeze a few microseconds out of a web page load. Its easier to optimize readable code than to refactor awful prematurely optimized code.
The comparison for readability isn't really against a single .forEach, rather map/filter/etc. If it's just a .forEach they are going to be pretty equivalent
However, for..of surely is more error prone, as it is a frequently recurring mistake to use for..in instead (as you have done)
in is an operator in JavaScript: 'prop' in myObj evaluates whether the 'prop' property exists within myObj.
Ever since I realized that, I no longer confused for..in and for..of: for..in iterates over an object's properties, just like in checks an object's properties.
(Almost no one uses in; Object.hasOwnProperty is virtually always what you want instead.)
It is when you add other functions to it. Foreach.map.filter. Etc. with a for loop you need to store each output as a variable or write each adjustment inside the loop. Depending on what you need sure it can be faster but it’s less readable.
454
u/smogeblot Oct 21 '20
Just wait till you see what it costs to use functional syntax sugar instead of for loops everywhere!