Warum ist die letztere Funktion 10% schneller, obwohl die Variablen immer wieder erstellt werden müssen?

14
var toSizeString = (function() {

 var KB = 1024.0,
     MB = 1024 * KB,
     GB = 1024 * MB;

  return function(size) {
    var gbSize = size / GB,
        gbMod  = size % GB,
        mbSize = gbMod / MB,
        mbMod  = gbMod % MB,
        kbSize = mbMod / KB;

    if (Math.floor(gbSize)) {
      return gbSize.toFixed(1) + 'GB';
    } else if (Math.floor(mbSize)) {
      return mbSize.toFixed(1) + 'MB';
    } else if (Math.floor(kbSize)) {
      return kbSize.toFixed(1) + 'KB';
    } else {
      return size + 'B';
    }
  };
})();

Und die schnellere Funktion: (Beachten Sie, dass immer wieder die gleichen Variablen kb / mb / gb berechnet werden müssen). Wo gewinnt es Leistung?

function toSizeString (size) {

 var KB = 1024.0,
     MB = 1024 * KB,
     GB = 1024 * MB;

 var gbSize = size / GB,
     gbMod  = size % GB,
     mbSize = gbMod / MB,
     mbMod  = gbMod % MB,
     kbSize = mbMod / KB;

 if (Math.floor(gbSize)) {
      return gbSize.toFixed(1) + 'GB';
 } else if (Math.floor(mbSize)) {
      return mbSize.toFixed(1) + 'MB';
 } else if (Math.floor(kbSize)) {
      return kbSize.toFixed(1) + 'KB';
 } else {
      return size + 'B';
 }
};
Zu meinem
quelle
3
In jeder statisch typisierten Sprache würden die "Variablen" als Konstanten kompiliert. Vielleicht sind moderne JS-Motoren in der Lage, die gleiche Optimierung durchzuführen. Dies scheint nicht zu funktionieren, wenn die Variablen Teil eines Abschlusses sind.
USR
6
Dies ist ein Implementierungsdetail der von Ihnen verwendeten JavaScript-Engine. Die theoretische Zeit und der theoretische Raum sind die gleichen. Nur die Implementierung einer bestimmten JavaScript-Engine kann diese variieren. Um Ihre Frage richtig zu beantworten, müssen Sie die spezifische JavaScript-Engine auflisten, mit der Sie diese gemessen haben. Vielleicht kennt jemand die Details seiner Implementierung, um zu sagen, wie / warum es eines optimaler gemacht hat als das andere. Sie sollten auch Ihren Messcode posten.
Jimmy Hoffa
Sie verwenden das Wort "compute" in Bezug auf konstante Werte. In dem, worauf Sie sich beziehen, gibt es wirklich nichts zu berechnen . Die Arithmetik konstanter Werte ist eine der einfachsten und offensichtlichsten Optimierungsmethoden, die Compiler ausführen. Wenn Sie also einen Ausdruck sehen, der nur konstante Werte enthält, können Sie davon ausgehen, dass der gesamte Ausdruck auf einen einzigen konstanten Wert optimiert ist.
Jimmy Hoffa
@ JimmyHoffa das ist wahr, aber auf der anderen Seite muss es 3 konstante Variablen für jeden Funktionsaufruf erstellen ...
Tomy
@Tomy-Konstanten sind keine Variablen. Sie variieren nicht und müssen daher nach dem Kompilieren nicht neu erstellt werden. Eine Konstante wird im Allgemeinen im Speicher abgelegt, und jeder zukünftige Zugriff auf diese Konstante wird genau an die gleiche Stelle geleitet. Sie muss nicht neu erstellt werden, da ihr Wert niemals variiert und daher keine Variable ist. Compiler geben im Allgemeinen keinen Code aus, der Konstanten erstellt . Der Compiler führt die Erstellung durch und leitet alle Code-Verweise auf das, was er erstellt hat, weiter.
Jimmy Hoffa

Antworten:

23

Moderne JavaScript-Engines kompilieren allesamt just-in-time. Man kann keine Vermutungen darüber anstellen, was es "immer und immer wieder schaffen muss". Diese Art der Berechnung ist in beiden Fällen relativ einfach zu optimieren.

Andererseits ist das Schließen über konstante Variablen kein typischer Fall, für den Sie die JIT-Kompilierung anstreben würden. In der Regel erstellen Sie einen Abschluss, wenn Sie diese Variablen bei verschiedenen Aufrufen ändern möchten. Sie erstellen auch eine zusätzliche Zeiger-Dereferenzierung, um auf diese Variablen zuzugreifen, z. B. den Unterschied zwischen dem Zugriff auf eine Mitgliedsvariable und ein lokales int in OOP.

Diese Art von Situation ist der Grund, warum die Leute die Linie "vorzeitige Optimierung" streichen. Die einfachen Optimierungen werden bereits vom Compiler durchgeführt.

Karl Bielefeldt
quelle
Ich vermute, es ist das Scope Traversal für variable Auflösung, das den Verlust verursacht, wie Sie erwähnen. Scheint vernünftig, aber wer weiß wirklich, was Wahnsinn in einer JavaScript-JIT-Engine liegt ...
Jimmy Hoffa
1
Mögliche Erweiterung dieser Antwort: Der Grund, warum eine JIT eine für einen Offline-Compiler einfache Optimierung ignorieren würde, ist, dass die Leistung des gesamten Compilers wichtiger ist als ungewöhnliche Fälle.
Leushenko
12

Variablen sind billig. Ausführungskontexte und Umfangsketten sind teuer.

Es gibt verschiedene Antworten, die im Wesentlichen auf "weil Abschlüsse" hinauslaufen, und diese sind im Wesentlichen wahr, aber das Problem betrifft nicht speziell den Abschluss, sondern die Tatsache, dass Sie eine Funktion haben, die auf Variablen in einem anderen Bereich verweist. Sie hätten das gleiche Problem, wenn dies globale Variablen auf dem wärenwindow Objekt , im Gegensatz zu lokalen Variablen innerhalb des IIFE. Probieren Sie es aus und sehen Sie.

Also in Ihrer ersten Funktion, wenn die Engine diese Anweisung sieht:

var gbSize = size / GB;

Es müssen folgende Schritte ausgeführt werden:

  1. Suche nach einer Variablen sizeim aktuellen Bereich. (Fand es.)
  2. Suche nach einer Variablen GBim aktuellen Bereich. (Nicht gefunden.)
  3. Suchen Sie GBim übergeordneten Bereich nach einer Variablen . (Fand es.)
  4. Berechnen und zuweisen gbSize.

Schritt 3 ist erheblich teurer als nur die Zuweisung einer Variablen. Darüber hinaus tun Sie dies fünfmal , einschließlich zweimal für beide GBund MB. Ich vermute, dass, wenn Sie diese zu Beginn der Funktion Alias ​​(zvar gb = GB ) und stattdessen auf den Alias ​​verweisen, dies tatsächlich zu einer geringen Beschleunigung führt, obwohl es auch möglich ist, dass einige JS-Engines diese Optimierung bereits durchführen. Und der effektivste Weg, die Ausführung zu beschleunigen, besteht natürlich darin, die Scope-Kette überhaupt nicht zu durchlaufen.

Beachten Sie, dass JavaScript nicht wie eine kompilierte, statisch typisierte Sprache ist, in der der Compiler diese variablen Adressen zur Kompilierungszeit auflöst. Die JS-Engine muss sie nach Namen auflösen , und diese Suchvorgänge werden jedes Mal zur Laufzeit ausgeführt. Sie sollten sie daher nach Möglichkeit meiden.

Die variable Zuweisung ist in JavaScript extrem günstig. Es könnte tatsächlich die billigste Operation sein, obwohl ich nichts habe, um diese Aussage zu stützen. Trotzdem kann man mit Sicherheit sagen, dass es fast nie eine gute Idee ist, das Erstellen von Variablen zu vermeiden . Fast jede Optimierung, die Sie in diesem Bereich vornehmen, wird die Leistung sogar noch verschlechtern.

Aaronaught
quelle
Und selbst wenn die „Optimierung“ nicht negativ die Leistung auswirken, ist es fast sicher ist , geht die Lesbarkeit des Codes negativ beeinflussen. Was, es sei denn, Sie machen ein paar verrückte Berechnungen, meistens ein schlechter Kompromiss ist (anscheinend leider kein Permalink-Anker; suchen Sie nach "2009-02-17 11:41"). Wie die Zusammenfassung dort lautet
ein Lebenslauf vom
Selbst wenn ein sehr einfacher Interpreter für dynamische Sprachen geschrieben wird, ist der variable Zugriff zur Laufzeit in der Regel eine O (1) -Operation, und das Durchlaufen des O (n) -Bereichs ist während der ersten Kompilierung nicht einmal erforderlich. In jedem Gültigkeitsbereich wird jeder neu deklarierten Variablen eine Nummer zugewiesen, damit var a, b, cwir auf sie zugreifen können bals scope[1]. Alle Bereiche sind nummeriert. Wenn dieser Bereich fünf Bereiche tief verschachtelt ist, bwird er vollständig angesprochen, env[5][1]was beim Parsen bekannt ist. Im nativen Code entsprechen Bereiche Stapelsegmenten. Verschlüsse sind etwas komplizierter, da sie denenv
amon
@amon: Vielleicht möchten Sie, dass es so funktioniert, aber so funktioniert es eigentlich nicht. Menschen, die weitaus sachkundiger und erfahrener sind, als ich Bücher darüber geschrieben habe; Insbesondere möchte ich Sie auf High Performance JavaScript von Nicholas C. Zakas hinweisen. Hier ist ein Ausschnitt , und er hat auch ein Gespräch mit Benchmarks geführt, um es zu sichern. Natürlich ist er nicht der einzige, sondern der bekannteste. JavaScript hat einen lexikalischen Geltungsbereich, daher sind Verschlüsse eigentlich nichts Besonderes - im Grunde genommen ist alles ein Verschluss.
Aaronaught
@Aaronaught Interessant. Da dieses Buch 5 Jahre alt ist, war ich daran interessiert, wie eine aktuelle JS-Engine mit variablen Suchvorgängen umgeht, und habe mir das x64-Backend der V8-Engine angesehen. Während der statischen Analyse werden die meisten Variablen statisch aufgelöst und erhalten in ihrem Bereich einen Speicheroffset. Funktionsbereiche werden als verknüpfte Listen dargestellt, und die Assembly wird als entrollte Schleife ausgegeben, um den richtigen Bereich zu erreichen. Hier würden wir das Äquivalent zum C-Code *(scope->outer + variable_offset)für einen Zugriff erhalten; Jede zusätzliche Funktionsumfangsebene kostet eine zusätzliche Zeiger-Dereferenzierung. Scheint, wir hatten beide recht :)
amon
2

Bei einem Beispiel handelt es sich um eine Schließung, bei dem anderen nicht. Das Implementieren von Closures ist etwas knifflig, da Closed-Over-Variablen nicht wie normale Variablen funktionieren. Dies ist in einer einfachen Sprache wie C offensichtlicher, aber ich werde JavaScript verwenden, um dies zu veranschaulichen.

Ein Closure besteht nicht nur aus einer Funktion, sondern auch aus allen Variablen, über die es geschlossen hat. Wenn wir diese Funktion aufrufen möchten, müssen wir auch alle Variablen bereitstellen, die geschlossen sind. Wir können einen Abschluss durch eine Funktion modellieren, die ein Objekt als erstes Argument empfängt, das diese über Variablen geschlossenen Werte darstellt:

function add(vars, y) {
  vars.x += y;
}

function getSum(vars) {
  return vars.x;
}

function makeAdder(x) {
  return { x: x, add: add, getSum: getSum };
}

var adder = makeAdder(40);
adder.add(adder, 2);
console.log(adder.getSum(adder));  //=> 42

Beachten Sie die closure.apply(closure, ...realArgs)dazu erforderliche umständliche Aufrufkonvention

Die integrierte Objektunterstützung von JavaScript ermöglicht es, das explizite varsArgument wegzulassen und thisstattdessen Folgendes zu verwenden :

function add(y) {
  this.x += y;
}

function getSum() {
  return this.x;
}

function makeAdder(x) {
  return { x: x, add: add, getSum: getSum };
}

var adder = makeAdder(40);
adder.add(2);
console.log(adder.getSum());  //=> 42

Diese Beispiele entsprechen dem Code, der tatsächlich Abschlüsse verwendet:

function makeAdder(x) {
  return {
    add: function (y) { x += y },
    getSum: function () { return x },
  };
}

var adder = makeAdder(40);
adder.add(2);
console.log(adder.getSum());  //=> 42

In diesem letzten Beispiel wird das Objekt nur zum Gruppieren der beiden zurückgegebenen Funktionen verwendet. Die thisBindung ist irrelevant. Alle Details zur Ermöglichung von Schließungen - die Übergabe versteckter Daten an die eigentliche Funktion, die Änderung aller Zugriffe auf Schließungsvariablen, um in diesen versteckten Daten nachzuschlagen - werden von der Sprache erledigt.

Das Aufrufen von Closures bedeutet jedoch den Mehraufwand für das Übergeben dieser zusätzlichen Daten, und das Ausführen eines Closures bedeutet den Mehraufwand für das Nachschlagen dieser zusätzlichen Daten - der durch eine schlechte Cache-Lokalität und normalerweise eine Zeiger-Dereferenzierung im Vergleich zu normalen Variablen verschlechtert wird -, so dass dies nicht verwunderlich ist Eine Lösung, die nicht auf Verschlüssen beruht, bietet eine bessere Leistung. Zumal alles, was Sie mit Ihrem Verschluss ersparen, ein paar extrem kostengünstige Rechenoperationen sind, die beim Parsen sogar konstant gefaltet werden können.

amon
quelle