Почему 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
Примечание. Поместите здесь, так как у меня не было достаточно очков, чтобы прокомментировать принятый ответ.