Warum ist "lassen" mit lexikalischem Umfang schneller?

31

Beim Lesen des Quellcodes für das dolistMakro bin ich auf den folgenden Kommentar gestoßen.

;; Dies ist kein zuverlässiger Test, aber es spielt keine Rolle, da beide Semantiken akzeptabel sind, wobei eine bei dynamischem Scoping etwas schneller ist und die andere bei lexikalischem Scoping etwas schneller ist (und eine sauberere Semantik aufweist) .

Was sich auf dieses Snippet bezog (das ich aus Gründen der Übersichtlichkeit vereinfacht habe).

(if lexical-binding
    (let ((temp list))
      (while temp
        (let ((it (car temp)))
          ;; Body goes here
          (setq temp (cdr temp)))))
  (let ((temp list)
        it)
    (while temp
      (setq it (car temp))
      ;; Body goes here
      (setq temp (cdr temp)))))

Es überraschte mich, dass ein letFormular in einer Schleife verwendet wurde. Früher dachte ich , dass langsam wiederholt mit im Vergleich ist setqauf den gleichen externen Variablen (wie auf dem zweiten Fall , der oben den Fall ist).

Ich hätte das als nichts abgetan, wenn nicht für den Kommentar direkt darüber ausdrücklich gesagt worden wäre, dass dies schneller ist als die Alternative (mit lexikalischer Bindung). Also ... warum ist das so?

  1. Warum unterscheidet sich der obige Code in der Leistung bei lexikalischer und dynamischer Bindung?
  2. Warum ist die letForm mit lexikalisch schneller?
Malabarba
quelle

Antworten:

38

Lexikalische Bindung versus dynamische Bindung im Allgemeinen

Betrachten Sie das folgende Beispiel:

(let ((lexical-binding nil))
  (disassemble
   (byte-compile (lambda ()
                   (let ((foo 10))
                     (message foo))))))

Es kompiliert und zerlegt eine einfache lambdamit einer lokalen Variablen. Bei lexical-bindingdeaktivierter Option sieht der Bytecode wie folgt aus:

0       constant  10
1       varbind   foo
2       constant  message
3       varref    foo
4       call      1
5       unbind    1
6       return    

Beachten Sie die Anweisungen varbindund varref. Diese Anweisungen binden bzw. suchen Variablen nach ihrem Namen in einer globalen Bindungsumgebung im Heapspeicher . All dies wirkt sich nachteilig auf die Leistung aus: Es umfasst das Hashing und Vergleichen von Zeichenfolgen , die Synchronisierung für den globalen Datenzugriff und den wiederholten Heap-Speicherzugriff , der sich schlecht auf das CPU-Caching auswirkt. Außerdem müssen dynamische Variablenbindungen werden gestellt , um ihre vorherigen Variable am Ende let, das fügt nfür jeden weitere Lookups letBlock mit nBindungen.

Wenn Sie im obigen Beispiel eine Bindung herstellen lexical-binding, tsieht der Bytecode etwas anders aus:

0       constant  10
1       constant  message
2       stack-ref 1
3       call      1
4       return    

Beachten Sie, dass varbindund varrefvöllig weg sind. Die lokale Variable wird einfach auf den Stack geschoben und über die stack-refAnweisung durch einen konstanten Offset referenziert. Im Wesentlichen wird die Variable mit konstanter Zeit gebunden und gelesen , der In-Stack-Speicher liest und schreibt, was vollständig lokal ist und daher gut mit Parallelität und CPU-Caching funktioniert und überhaupt keine Zeichenfolgen umfasst.

Im Allgemeinen mit lexikalischer Bindung Lookups von lokalen Variablen (zB let, setqusw.) hat viel weniger Laufzeit und Speicherkomplexität .

Dieses konkrete Beispiel

Bei dynamischer Bindung ist aus den oben genannten Gründen für jede Überlassung eine Leistungsstrafe zu zahlen. Je mehr Lets, desto dynamischer die Variablenbindungen.

Bemerkenswert ist , mit einer zusätzlichen letim loopKörper, würde der gebundenen Variablen müssen bei wiederhergestellt werden jeder Iteration der Schleife , eine Zugabe von zusätzlichen variablen Lookup jeder Iteration . Daher ist es schneller, das Loslassen aus dem Schleifenkörper herauszuhalten, sodass die Iterationsvariable nur einmal zurückgesetzt wird , nachdem die gesamte Schleife beendet ist. Dies ist jedoch nicht besonders elegant, da die Iterationsvariable weit gebunden ist, bevor sie tatsächlich benötigt wird.

Mit lexikalischer Bindung sind lets billig. Insbesondere ist ein letinnerhalb eines Schleifenkörpers nicht schlechter (leistungsmäßig) als ein letaußerhalb eines Schleifenkörpers. Daher ist es in Ordnung, Variablen so lokal wie möglich zu binden und die Iterationsvariable auf den Schleifenkörper zu beschränken.

Es ist auch etwas schneller, weil es zu viel weniger Anweisungen kompiliert. Beachten Sie die nachfolgende Seite-an-Seite-Demontage (örtliche Genehmigung auf der rechten Seite):

0       varref    list            0       varref    list         
1       constant  nil             1:1     dup                    
2       varbind   it              2       goto-if-nil-else-pop 2 
3       dup                       5       dup                    
4       varbind   temp            6       car                    
5       goto-if-nil-else-pop 2    7       stack-ref 1            
8:1     varref    temp            8       cdr                    
9       car                       9       discardN-preserve-tos 2
10      varset    it              11      goto      1            
11      varref    temp            14:2    return                 
12      cdr       
13      dup       
14      varset    temp
15      goto-if-not-nil 1
18      constant  nil
19:2    unbind    2
20      return    

Ich habe jedoch keine Ahnung, was den Unterschied verursacht.

Mondhorn
quelle
7

Kurz gesagt - die dynamische Bindung ist sehr langsam. Die lexikalische Bindung ist zur Laufzeit extrem schnell. Der Grund dafür ist, dass die lexikalische Bindung zur Kompilierungszeit aufgelöst werden kann, während die dynamische Bindung dies nicht kann.

Betrachten Sie den folgenden Code:

(let ((x 42))
    (foo)
    (message "%d" x))

Beim Kompilieren von letkann der Compiler nicht wissen, ob er fooauf die (dynamisch gebundene) Variable zugreifen wird. Daher xmuss er eine Bindung für xdie Variable erstellen und den Namen der Variablen beibehalten. Bei der lexikalischen Bindung gibt der Compiler nur den Wert des xBindungsstapels ohne Namen aus und greift direkt auf den richtigen Eintrag zu.

Aber warte - es gibt noch mehr. Mit der lexikalischen Bindung kann der Compiler überprüfen, ob diese bestimmte Bindung xnur im Code to verwendet wird message. Da xes niemals modifiziert wird, ist es sicher zu inline xund nachzugeben

(progn
  (foo)
  (message "%d" 42))

Ich glaube nicht, dass der aktuelle Bytecode-Compiler diese Optimierung durchführt, aber ich bin zuversichtlich, dass dies in Zukunft der Fall sein wird.

Also in Kürze:

  • Die dynamische Bindung ist eine schwere Operation, die nur wenige Optimierungsmöglichkeiten bietet.
  • lexikalische Bindung ist eine leichte Operation;
  • Die lexikalische Bindung eines Nur-Lese-Werts kann häufig wegoptimiert werden.
jch
quelle
3

Dieser Kommentar deutet nicht darauf hin, dass die lexikalische Bindung schneller oder langsamer als die dynamische Bindung ist. Vielmehr deutet dies darauf hin, dass diese unterschiedlichen Formen unter lexikalischer und dynamischer Bindung unterschiedliche Leistungsmerkmale aufweisen, z. B. ist eine unter einer Bindungsdisziplin und die andere unter der anderen bevorzugt.

So ist lexikalischer Gültigkeitsbereich schneller als Dynamikumfang? Ich vermute, dass es in diesem Fall keinen großen Unterschied gibt, aber ich weiß es nicht - man müsste es wirklich messen.

gsg
quelle
1
Es gibt keinen varbindCode, der unter lexikalischer Bindung kompiliert wurde. Das ist der springende Punkt und Zweck.
Lunaryorn
Hmm. Ich habe eine Datei mit der obigen Quelle erstellt, angefangen mit ;; -*- lexical-binding: t -*-, sie geladen und aufgerufen (byte-compile 'sum1), unter der Annahme, dass eine unter lexikalischer Bindung kompilierte Definition erstellt wurde. Es scheint jedoch nicht zu haben.
gsg
Die Bytecode-Kommentare wurden entfernt, da sie auf dieser falschen Annahme basierten.
gsg
lunaryon Antwort zeigt , dass dieser Code eindeutig ist schneller unter lexikalischer Bindung (obwohl natürlich nur auf der Mikroebene).
Shosti
@gsg Diese Deklaration ist nur eine Standarddateivariable, die keine Auswirkung auf Funktionen hat, die von außerhalb des entsprechenden Dateipuffers aufgerufen werden. IOW, es hat nur Auswirkungen, wenn Sie die Quelldatei besuchen und dann byte-compilemit dem entsprechenden aktuellen Puffer aufrufen , was übrigens genau das ist, was der Byte-Compiler tut. Wenn Sie byte-compileseparat aufrufen , müssen Sie explizit festlegen lexical-binding, wie ich es in meiner Antwort getan habe.
Lunaryorn