TL; DR Die langsamere Schleife ist auf den Zugriff auf das Array außerhalb der Grenzen zurückzuführen, wodurch die Engine entweder gezwungen wird, die Funktion mit weniger oder gar keinen Optimierungen neu zu kompilieren, oder die Funktion zunächst nicht mit einer dieser Optimierungen zu kompilieren ( Wenn der (JIT-) Compiler diesen Zustand vor der ersten Kompilierungsversion erkannt / vermutet hat, lesen Sie weiter unten, warum;
Jemand hat gerade
hat , dies zu sagen (ganz erstaunt niemand bereits getan haben):
Früher gab es eine Zeit , in der das Snippet des OP wäre eine de-facto - Beispiel in einem Anfänger Programmierung Buch Umriss bestimmt sein / betonen , dass ‚Arrays‘ in Javascript indexierten Ausgangs sind bei 0, nicht 1, und als solches als Beispiel für einen häufigen "Anfängerfehler" verwendet werden (lieben Sie es nicht, wie ich den Ausdruck "Programmierfehler" vermieden habe
;)
):
Außerhalb der Grenzen Array-Zugriff .
Beispiel 1:
a Dense Array
(zusammenhängend (bedeutet keine Lücken zwischen den Indizes) UND tatsächlich ein Element an jedem Index) von 5 Elementen unter Verwendung einer 0-basierten Indizierung (immer in ES262).
var arr_five_char=['a', 'b', 'c', 'd', 'e']; // arr_five_char.length === 5
// indexes are: 0 , 1 , 2 , 3 , 4 // there is NO index number 5
Wir sprechen also nicht wirklich über Leistungsunterschiede zwischen <
vs <=
(oder "eine zusätzliche Iteration"), aber wir sprechen über:
"Warum läuft das richtige Snippet (b) schneller als das fehlerhafte Snippet (a)"?
Die Antwort ist zweifach (obwohl aus Sicht eines ES262-Sprachimplementierers beide Formen der Optimierung sind):
- Datendarstellung: Wie wird das Array intern im Speicher dargestellt / gespeichert (Objekt, Hashmap, 'reales' numerisches Array usw.)?
- Funktionaler Maschinencode: Kompilieren des Codes, der auf diese 'Arrays' zugreift / sie handhabt (liest / ändert).
Punkt 1 wird durch die akzeptierte Antwort ausreichend (und meiner Meinung nach korrekt) erklärt , aber das verbraucht nur 2 Wörter ('der Code') Punkt 2: Zusammenstellung .
Genauer gesagt: JIT-Compilation und vor allem JIT- RE- Compilation!
Die Sprachspezifikation ist im Grunde nur eine Beschreibung einer Reihe von Algorithmen ("Schritte zur Erzielung eines definierten Endergebnisses"). Wie sich herausstellt, ist dies eine sehr schöne Art, eine Sprache zu beschreiben. Die eigentliche Methode, mit der eine Engine bestimmte Ergebnisse erzielt, bleibt den Implementierern offen. Dies bietet ausreichend Gelegenheit, effizientere Methoden zur Erstellung definierter Ergebnisse zu entwickeln. Ein spezifikationskonformer Motor sollte spezifikationskonforme Ergebnisse für jede definierte Eingabe liefern.
Jetzt, da Javascript-Code / Bibliotheken / Nutzung zunimmt und sich daran erinnert, wie viel Ressourcen (Zeit / Speicher / usw.) ein "echter" Compiler verbraucht, ist es klar, dass Benutzer, die eine Webseite besuchen, nicht so lange warten können (und diese benötigen) so viele Ressourcen zur Verfügung zu haben).
Stellen Sie sich die folgende einfache Funktion vor:
function sum(arr){
var r=0, i=0;
for(;i<arr.length;) r+=arr[i++];
return r;
}
Perfekt klar, oder? Benötigt keine zusätzliche Klarstellung, oder? Der Rückgabetyp istNumber
, richtig?
Nun ... nein, nein & nein ... Es hängt davon ab, welches Argument Sie an den benannten Funktionsparameter übergeben arr
...
sum('abcde'); // String('0abcde')
sum([1,2,3]); // Number(6)
sum([1,,3]); // Number(NaN)
sum(['1',,3]); // String('01undefined3')
sum([1,,'3']); // String('NaN3')
sum([1,2,{valueOf:function(){return this.val}, val:6}]); // Number(9)
var val=5; sum([1,2,{valueOf:function(){return val}}]); // Number(8)
Sehen Sie das Problem? Dann denken Sie daran, dass dies kaum die massiven möglichen Permutationen abkratzt ... Wir wissen nicht einmal, welche Art von TYP die Funktion RETURN ist, bis wir fertig sind ...
Stellen Sie sich nun dieselbe Funktion vor: Code tatsächlich auf verschiedenen Typen oder auch Variationen von Eingabe verwendet werden, die beide vollständig wahrsten Sinne des Wortes (im Quellcode) beschrieben und dynamisch-Programm generiert ‚Arrays‘ ..
Also, wenn Sie Funktion kompilieren würden sum
NUR EINMAL , kann der einzige Weg, der immer das spezifikationsdefinierte Ergebnis für alle Arten von Eingaben zurückgibt, natürlich nur durch Ausführen ALLER spezifikationsbedingten Haupt- UND Unterschritte spezifikationskonforme Ergebnisse garantieren (wie ein unbenannter Pre-Y2K-Browser). Es verbleiben keine Optimierungen (weil keine Annahmen) und eine absolut langsam interpretierte Skriptsprache.
JIT-Compilation (JIT wie in Just In Time) ist die derzeit beliebte Lösung.
Sie beginnen also mit dem Kompilieren der Funktion unter Verwendung von Annahmen darüber, was sie tut, zurückgibt und akzeptiert.
Sie prüfen so einfach wie möglich, ob die Funktion möglicherweise nicht spezifikationskonforme Ergebnisse zurückgibt (z. B. weil sie unerwartete Eingaben erhält). Werfen Sie dann das zuvor kompilierte Ergebnis weg und kompilieren Sie es erneut, um zu entscheiden, was mit dem bereits vorhandenen Teilergebnis geschehen soll (ist es gültig, vertrauenswürdig zu sein oder erneut zu berechnen, um sicherzugehen), binden Sie die Funktion wieder in das Programm ein und Versuch es noch einmal. Letztendlich wird auf die schrittweise Skriptinterpretation wie in der Spezifikation zurückgegriffen.
All dies braucht Zeit!
Alle Browser arbeiten an ihren Engines. Für jede Unterversion werden sich die Dinge verbessern und zurückbilden. Strings waren irgendwann in der Geschichte wirklich unveränderliche Strings (daher war array.join schneller als String-Verkettung), jetzt verwenden wir Seile (oder ähnliches), die das Problem lindern. Beide liefern spezifikationskonforme Ergebnisse und darauf kommt es an!
Lange Rede, kurzer Sinn: Nur weil die Semantik der Sprache von Javascript uns oft den Rücken gekehrt hat (wie bei diesem stillen Fehler im Beispiel des OP), bedeutet dies nicht, dass 'dumme' Fehler unsere Chancen erhöhen, dass der Compiler schnellen Maschinencode ausspuckt. Es wird davon ausgegangen, dass wir die 'normalerweise' korrekten Anweisungen geschrieben haben: Das aktuelle Mantra, das wir 'Benutzer' (der Programmiersprache) haben müssen, lautet: Helfen Sie dem Compiler, beschreiben Sie, was wir wollen, bevorzugen Sie gängige Redewendungen (nehmen Sie Hinweise von asm.js für ein grundlegendes Verständnis welche Browser versuchen können zu optimieren und warum).
Aus diesem Grund ist es wichtig, über Leistung zu sprechen, ABER AUCH ein Minenfeld (und wegen dieses Minenfeldes möchte ich wirklich damit enden, auf relevantes Material zu verweisen (und es zu zitieren):
Der Zugriff auf nicht vorhandene Objekteigenschaften und Array-Elemente außerhalb der Grenzen gibt den undefined
Wert zurück, anstatt eine Ausnahme auszulösen. Diese dynamischen Funktionen machen das Programmieren in JavaScript bequem, erschweren aber auch das Kompilieren von JavaScript zu effizientem Maschinencode.
...
Eine wichtige Voraussetzung für eine effektive JIT-Optimierung ist, dass Programmierer die dynamischen Funktionen von JavaScript systematisch nutzen. Beispielsweise nutzen JIT-Compiler die Tatsache, dass Objekteigenschaften häufig zu einem Objekt eines bestimmten Typs in einer bestimmten Reihenfolge hinzugefügt werden oder dass Array-Zugriffe außerhalb der Grenzen selten auftreten. JIT-Compiler nutzen diese Regelmäßigkeitsannahmen, um zur Laufzeit effizienten Maschinencode zu generieren. Wenn ein Codeblock die Annahmen erfüllt, führt die JavaScript-Engine effizienten, generierten Maschinencode aus. Andernfalls muss die Engine auf langsameren Code oder auf die Interpretation des Programms zurückgreifen.
Quelle:
"JITProf: Auffinden von JIT-unfreundlichem JavaScript-Code"
Berkeley-Veröffentlichung, 2014, von Liang Gong, Michael Pradel, Koushik Sen.
http://software-lab.org/publications/jitprof_tr_aug3_2014.pdf
ASM.JS (mag auch keinen Off-Bound-Array-Zugriff):
Vorzeitige Zusammenstellung
Da asm.js eine strikte Teilmenge von JavaScript ist, definiert diese Spezifikation nur die Validierungslogik - die Ausführungssemantik ist einfach die von JavaScript. Validierte asm.js können jedoch vorab kompiliert werden (AOT). Darüber hinaus kann der von einem AOT-Compiler generierte Code sehr effizient sein und Folgendes umfassen:
- Unboxed-Darstellungen von Ganzzahlen und Gleitkommazahlen;
- Fehlen von Laufzeit-Typprüfungen;
- Fehlen einer Müllabfuhr; und
- Effiziente Heap-Ladevorgänge und -Speicher (wobei die Implementierungsstrategien je nach Plattform variieren).
Code, der nicht validiert werden kann, muss mit herkömmlichen Mitteln, z. B. Interpretation und / oder Just-in-Time-Kompilierung (JIT), auf die Ausführung zurückgreifen.
http://asmjs.org/spec/latest/
und schließlich https://blogs.windows.com/msedgedev/2015/05/07/bringing-asm-js-to-chakra-microsoft-edge/
, wo es einen kleinen Unterabschnitt über die internen Leistungsverbesserungen des Motors beim Entfernen von Grenzen gibt. check (während nur der Bounds-Check außerhalb der Schleife angehoben wurde, wurde bereits eine Verbesserung von 40% erzielt).
BEARBEITEN:
Beachten Sie, dass mehrere Quellen über verschiedene Ebenen der JIT-Neukompilierung bis hin zur Interpretation sprechen.
Theoretisches Beispiel basierend auf den obigen Informationen zum OP-Snippet:
- Rufen Sie isPrimeDivisible auf
- Kompilieren Sie isPrimeDivisible unter Verwendung allgemeiner Annahmen (z. B. kein Zugriff außerhalb der Grenzen).
- Arbeite
- BAM, plötzlich Array-Zugriffe außerhalb der Grenzen (direkt am Ende).
- Mist, sagt Engine, lassen Sie uns dasPrimeDivisible mit anderen (weniger) Annahmen neu kompilieren, und diese Beispiel-Engine versucht nicht herauszufinden, ob sie das aktuelle Teilergebnis wiederverwenden kann
- Berechnen Sie alle Arbeiten mit der langsameren Funktion neu (hoffentlich ist sie beendet, andernfalls wiederholen Sie sie und interpretieren Sie diesmal nur den Code).
- Ergebnis zurückgeben
Daher war die Zeit dann:
Erster Lauf (am Ende fehlgeschlagen) + Wiederholen aller Arbeiten mit langsamerem Maschinencode für jede Iteration + Die Neukompilierung usw. dauert in diesem theoretischen Beispiel eindeutig> 2-mal länger !
EDIT 2: (Haftungsausschluss: Vermutung basierend auf den folgenden Fakten)
Je mehr ich darüber nachdenke, desto mehr denke ich, dass diese Antwort tatsächlich den dominanteren Grund für diese "Strafe" für fehlerhaftes Snippet a (oder Leistungsbonus für Snippet b) erklären könnte , je nachdem, wie Sie darüber denken), genau, warum ich es als Programmierfehler bezeichne (Snippet a):
Es ist ziemlich verlockend anzunehmen, dass this.primes
es sich um ein 'dichtes Array' handelt, das entweder rein numerisch ist
- Hartcodiertes Literal im Quellcode (bekannter hervorragender Kandidat, um ein "echtes" Array zu werden, da dem Compiler bereits vor der Kompilierungszeit alles bekannt ist ) ODER
- höchstwahrscheinlich generiert mit einer numerischen Funktion, die ein Pre-Size (
new Array(/*size value*/)
) in aufsteigender Reihenfolge füllt (ein weiterer seit langem bekannter Kandidat, um ein "reales" Array zu werden).
Wir wissen auch , dass die primes
Länge des Arrays wird zwischengespeichert , wie prime_count
! (zeigt die Absicht und die feste Größe an).
Wir wissen auch, dass die meisten Engines Arrays zunächst als Copy-on-Modify (bei Bedarf) übergeben, wodurch die Handhabung erheblich beschleunigt wird (wenn Sie sie nicht ändern).
Es ist daher anzunehmen, dass Array primes
höchstwahrscheinlich bereits ein intern optimiertes Array ist, das nach der Erstellung nicht geändert wird (für den Compiler einfach zu wissen, wenn kein Code vorhanden ist, der das Array nach der Erstellung ändert) und daher bereits vorhanden ist (falls zutreffend für der Motor) auf optimierte Weise gespeichert, so als wäre es ein Typed Array
.
Wie ich sum
anhand meines Funktionsbeispiels zu verdeutlichen versucht habe , beeinflussen die übergebenen Argumente stark, was tatsächlich passieren muss und wie dieser bestimmte Code zu Maschinencode kompiliert wird. Das Übergeben von a String
an die sum
Funktion sollte nicht die Zeichenfolge ändern, sondern die Art und Weise, wie die Funktion JIT-kompiliert ist! Wenn Sie ein Array an übergeben, sum
sollte eine andere (möglicherweise sogar zusätzliche für diesen Typ oder 'Form', wie sie es nennen, von Objekt, das übergeben wurde) Version des Maschinencodes kompiliert werden.
Da es ein wenig verrückt erscheint, das Typed_Array-ähnliche primes
Array im laufenden Betrieb in etwas anderes zu konvertieren , während der Compiler weiß, dass diese Funktion es nicht einmal ändern wird!
Unter diesen Annahmen bleiben 2 Optionen:
- Kompilieren Sie als Zahlenknacker unter der Annahme, dass keine Grenzen überschritten sind, stoßen Sie am Ende auf ein Problem außerhalb der Grenzen, kompilieren Sie die Arbeit neu und wiederholen Sie sie (wie im theoretischen Beispiel in Bearbeitung 1 oben beschrieben)
- Der Compiler hat bereits im Vorfeld einen gebundenen Zugriff erkannt (oder vermutet?) Und die Funktion wurde JIT-kompiliert, als ob das übergebene Argument ein spärliches Objekt wäre, was zu einem langsameren funktionalen Maschinencode führt (da es mehr Überprüfungen / Konvertierungen / Zwänge hätte etc.). Mit anderen Worten: Die Funktion war für bestimmte Optimierungen nie geeignet. Sie wurde so kompiliert, als ob sie ein '- spärliches Array' (- ähnliches) Argument erhalten hätte.
Ich frage mich jetzt wirklich, welche von diesen 2 es ist!
<=
und<
sind sowohl in der Theorie als auch in der tatsächlichen Implementierung in allen modernen Prozessoren (und Interpreten) identisch.