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 lambda
mit einer lokalen Variablen. Bei lexical-binding
deaktivierter 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 varbind
und 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 n
für jeden weitere Lookups let
Block mit n
Bindungen.
Wenn Sie im obigen Beispiel eine Bindung herstellen lexical-binding
, t
sieht der Bytecode etwas anders aus:
0 constant 10
1 constant message
2 stack-ref 1
3 call 1
4 return
Beachten Sie, dass varbind
und varref
völlig weg sind. Die lokale Variable wird einfach auf den Stack geschoben und über die stack-ref
Anweisung 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
, setq
usw.) 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 let
im loop
Kö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 let
s billig. Insbesondere ist ein let
innerhalb eines Schleifenkörpers nicht schlechter (leistungsmäßig) als ein let
auß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.
varbind
Code, der unter lexikalischer Bindung kompiliert wurde. Das ist der springende Punkt und Zweck.