Почему lodash.each быстрее, чем native forEach?

Я пытался найти самый быстрый способ запуска цикла for с его собственной областью. Три метода, которые я сравнивал, были:

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]);
}

Это на Chrome 29 на OS X. Вы можете сами запустить тесты:

http://jsben.ch/#/BQhED

Как lodash .each почти в два раза быстрее, чем native .forEach? И более того, как это происходит быстрее, чем обычная for? Волшебство? Черная магия?

Ответ 1

_.each() не полностью совместим с [].forEach(). См. Следующий пример:

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

http://jsfiddle.net/BhrT3/

В реализации lodash отсутствует проверка if (... in ...), которая может объяснить разницу в производительности.


Как отмечено в комментариях выше, разница с native for обусловлена ​​главным образом дополнительным поиском функций в вашем тесте. Используйте эту версию, чтобы получить более точные результаты:

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

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

Ответ 2

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

Разработчики lo-dash объясняют (здесь и на видео), что относительная скорость родного forEach варьируется среди браузеров. Просто потому, что forEach является родным, это не означает, что он быстрее, чем простой цикл, построенный с помощью for или while. Во-первых, forEach приходится иметь дело с более частыми случаями. Во-вторых, forEach использует обратные вызовы с (потенциальными) служебными функциями вызова функций и т.д.

chrome, в частности, известен (по крайней мере, разработчикам lo-dash) относительно медленный forEach. Поэтому для этого браузера lo-dash использует собственный простой цикл while для получения скорости. Следовательно, преимущество скорости, которое вы видите (но другие нет).

Разумно выбрав собственные методы - только используя собственный реализация, если она известна быстро в заданной среде - Lo-Dash позволяет избежать проблем с производительностью и согласованностью с туземцами.

Ответ 3

Да, lodash/underscore у каждого даже не имеют такой же семантики, как .forEach. Есть тонкие детали, которые сделают функцию очень медленной, если двигатель не сможет быстро проверить разреженные массивы без геттеров.

Это будет соответствовать 99% спецификации и работает с той же скоростью, что и lodash в V8 для обычного случая:

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;

Проверяя сначала undefined, мы вообще не наказываем нормальные массивы в цикле. Двигатель может использовать свои внутренние компоненты для обнаружения странных массивов, но V8 не делает.

Ответ 4

Здесь обновленная ссылка (около 2015 года) показывает разницу в производительности, которая сравнивает все три, for(...), Array.forEach и _.each: https://jsperf.com/native-vs-underscore-vs-lodash

Примечание. Поместите здесь, так как у меня не было достаточно очков, чтобы прокомментировать принятый ответ.