Why is lodash.each faster than native forEach?

JavascriptPerformanceLodash

Javascript Problem Overview


I was trying to find the fastest way of running a for loop with its own scope. The three methods I compared were:

var a = "t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t".split();

// lodash .each -> 1,294,971 ops/sec
lodash.each(a, function(item) { cb(item); });

// native .forEach -> 398,167 ops/sec
a.forEach(function(item) { cb(item); });

// native for -> 1,140,382 ops/sec
var lambda = function(item) { cb(item); };
for (var ix = 0, len = a.length; ix < len; ix++) {
  lambda(a[ix]);
}

This is on Chrome 29 on OS X. You can run the tests yourself here:

http://jsben.ch/BQhED

How is lodash's .each almost twice as fast as native .forEach? And moreover, how is it faster than the plain for? Sorcery? Black magic?

Javascript Solutions


Solution 1 - Javascript

_.each() is not fully compatible to [].forEach(). See the following example:

var a = ['a0'];
a[3] = 'a3';
_.each(a, console.log); // runs 4 times
a.forEach(console.log); // runs twice -- that's just how [].forEach() is specified

http://jsfiddle.net/BhrT3/

So lodash's implementation is missing an if (... in ...) check, which might explain the performance difference.


As noted in the comments above, the difference to native for is mainly caused by the additional function lookup in your test. Use this version to get more accurate results:

for (var ix = 0, len = a.length; ix < len; ix++) {
  cb(a[ix]);
}

http://jsperf.com/lo-dash-each-vs-native-foreach/15

Solution 2 - Javascript

http://kitcambridge.be/blog/say-hello-to-lo-dash/

The lo-dash developers explain (here and on a video) that the relative speed of the native forEach varies among browsers. Just because forEach is native does not mean that it is faster than a simple loop built with for or while. For one thing, the forEach has to deal with more special cases. Secondly, forEach uses callbacks, with the (potential) overhead of function invocation etc.

chrome in particular is known (at least to the lo-dash developers) to have a relatively slow forEach. So for that browser, lo-dash uses it's own simple while loop to gain speed. Hence the speed advantage that you see (but others don't).

> By smartly opting into native methods — only using a native > implementation if it’s known to be fast in a given environment — > Lo-Dash avoids the performance cost and consistency issues associated > with natives.

Solution 3 - Javascript

Yes, lodash/underscore each don't even have same semantics as .forEach. There is a subtle detail that will make the function really slow unless the engine can check for sparse arrays without getters quickly.

This will be 99% spec compliant and runs at the same speed as lodash each in V8 for the common case:

function FastAlmostSpecForEach( fn, ctx ) {
    "use strict";
    if( arguments.length > 1 ) return slowCaseForEach();
    if( typeof this !== "object" ) return slowCaseForEach();
    if( this === null ) throw new Error("this is null or not defined");
    if( typeof fn !== "function" ) throw new Error("is not a function");
    var len = this.length;
    if( ( len >>> 0 ) !== len ) return slowCaseForEach();
    
    
    for( var i = 0; i < len; ++i ) {
        var item = this[i];
        //Semantics are not exactly the same,
        //Fully spec compliant will not invoke getters
       //but this will.. however that is an insane edge case
        if( item === void 0 && !(i in this) ) {
            continue;
        }
        fn( item, i, this );
    }
}

Array.prototype.fastSpecForEach = FastAlmostSpecForEach;

By checking for undefined first, we don't punish normal arrays in the loop at all. An engine could use its internals to detect strange arrays but V8 doesn't.

Solution 4 - Javascript

Here's an updated link (circa 2015) showing the performance difference which compares all three, for(...), Array.forEach and _.each: https://jsperf.com/native-vs-underscore-vs-lodash

Note: Put here since I didn't have enough points yet to comment on the accepted answer.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMatt ZukowskiView Question on Stackoverflow
Solution 1 - Javascriptuser123444555621View Answer on Stackoverflow
Solution 2 - JavascripthpauljView Answer on Stackoverflow
Solution 3 - JavascriptEsailijaView Answer on Stackoverflow
Solution 4 - JavascriptpatricknelsonView Answer on Stackoverflow