Warum ist lodash.each für jeden schneller als native?

74

Ich habe versucht, den schnellsten Weg zu finden, um eine for-Schleife mit einem eigenen Bereich auszuführen. Die drei Methoden, die ich verglichen habe, waren:

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

Dies ist unter Chrome 29 unter OS X. Sie können die Tests hier selbst ausführen:

http://jsben.ch/BQhED

Wie ist Lodash .eachfast doppelt so schnell wie einheimisch .forEach? Und außerdem, wie ist es schneller als die Ebene for? Zauberei? Schwarze Magie?

Matt Zukowski
quelle
1
Wofür ist das lambdaDing? Warum setzen Sie nicht einfach cbdirekt?
Bergi
1
Lo-Dash .each()ist für mich viel langsamer als jede andere Methode in Ihrem Test. FF 23.0.1
Joe Simmons
1
Der Unterschied zwischen .each()und forergibt sich aus der zusätzlichen Funktionssuche ( lambda). Eine aussagekräftigere Benchmark finden Sie unter jsperf.com/lo-dash-each-vs-native-foreach/15 .
user123444555621
1
Ich habe 10000 Elemente zu Ihrem Test hinzugefügt und das Lodash ist jetzt langsamer als das native foreach: jsben.ch/RBkjH
kevinl

Antworten:

95

_.each()ist nicht vollständig kompatibel mit [].forEach(). Siehe folgendes Beispiel:

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/

Bei der Implementierung von lodash fehlt daher eine if (... in ...)Überprüfung, die den Leistungsunterschied erklären könnte.


Wie in den obigen Kommentaren erwähnt, wird der Unterschied zu native forhauptsächlich durch die zusätzliche Funktionssuche in Ihrem Test verursacht. Verwenden Sie diese Version, um genauere Ergebnisse zu erhalten:

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

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

user123444555621
quelle
1
Dank dafür. Es sei jedoch darauf hingewiesen, dass das forohne Verschluss nicht ganz gleichwertig ist. Das Innere des forBlocks erhält keinen eigenen Bereich.
Matt Zukowski
1
@MattZukowski Ihr ursprünglicher Testfall for (...) {(function(item) {cb(item);})(a[ix]);} ist in dieser Hinsicht in Ordnung. Im Gegensatz zu Ihrem obigen Kommentar verursacht dies nicht für jede Iteration einen erheblichen Overhead. Siehe auch
user123444555621
4
Lo-Dash erhält seine Geschwindigkeit, indem alle Arrays als dicht behandelt und aus der Schleife herausgerufen werden. Einige JS-Engines haben möglicherweise auch Probleme beim Inlining über die native Methodengrenze hinweg, die durch einfaches JS vermieden wird. Das Behandeln aller Arrays als dicht ist browserübergreifender, da IE <9 den Literalwert undefinedin einem Array wie [null, undefined, false]ein Loch behandelt und überspringt, während andere dies nicht tun.
John-David Dalton
24
Ich bekomme eine 404 auf diesem jsperfLink.
hpaulj
1
Ich bekomme auch 404. Hat jemand die Ergebnisse gesehen und welche waren schneller?
PrimeLens
24

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

Die Lo-Dash-Entwickler erklären (hier und in einem Video), dass die relative Geschwindigkeit des Native forEachzwischen den Browsern variiert. Nur weil forEaches nativ ist, heißt das nicht, dass es schneller ist als eine einfache Schleife, die mit foroder erstellt wurde while. Zum einen forEachmuss sich der mit mehr Sonderfällen befassen. Zweitens forEachwerden Rückrufe mit dem (potenziellen) Overhead des Funktionsaufrufs usw. verwendet.

chromeInsbesondere ist bekannt (zumindest den Lo-Dash-Entwicklern), dass sie relativ langsam sind forEach. Für diesen Browser verwendet lo-dash eine eigene einfache whileSchleife, um an Geschwindigkeit zu gewinnen. Daher der Geschwindigkeitsvorteil, den Sie sehen (andere jedoch nicht).

Durch die intelligente Auswahl nativer Methoden - eine native Implementierung wird nur verwendet, wenn bekannt ist, dass sie in einer bestimmten Umgebung schnell ist - vermeidet Lo-Dash die Leistungskosten und Konsistenzprobleme, die mit nativen Methoden verbunden sind.

hpaulj
quelle
18

Ja, lodash / Unterstrich haben nicht einmal die gleiche Semantik wie .forEach. Es gibt ein subtiles Detail, das die Funktion sehr langsam macht, es sei denn, die Engine kann ohne Getter schnell nach spärlichen Arrays suchen.

Dies ist zu 99% spezifikationskonform und läuft mit der gleichen Geschwindigkeit wie lodash in V8 für den allgemeinen Fall:

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;

Indem wir zuerst nach undefiniert suchen, bestrafen wir normale Arrays in der Schleife überhaupt nicht. Eine Engine könnte ihre Interna verwenden, um seltsame Arrays zu erkennen, V8 jedoch nicht.

Esailija
quelle
6

Hier ist ein aktualisierter Link (ca. 2015), der den Leistungsunterschied zeigt, der alle drei vergleicht for(...), Array.forEachund _.each: https://jsperf.com/native-vs-underscore-vs-lodash

Hinweis: Geben Sie hier ein, da ich noch nicht genügend Punkte hatte, um die akzeptierte Antwort zu kommentieren.

chunk_split
quelle
2
Es scheint also lodash.eachnicht mehr schneller zu sein als forEach(Chrome 78, 2020)
adelriosantiago
Lodash ist auf meinem Computer immer noch schneller (i9-9900k cpu @ 5Ghz 8core 16 thread) als native forEach, aber eine native for-Schleife ist doppelt so schnell wie beide (dies alles gemäß dem Ausführen des Tests unter der oben genannten URL).
agm1984