Reliure lexicale contre reliure dynamique en général
Prenons l'exemple suivant:
(let ((lexical-binding nil))
(disassemble
(byte-compile (lambda ()
(let ((foo 10))
(message foo))))))
Il compile et démonte immédiatement un simple lambda
avec une variable locale. Avec lexical-binding
désactivé, comme ci-dessus, le code d'octet se présente comme suit:
0 constant 10
1 varbind foo
2 constant message
3 varref foo
4 call 1
5 unbind 1
6 return
Notez les instructions varbind
et varref
. Ces instructions lient et recherchent respectivement des variables par leur nom dans un environnement de liaison global sur la mémoire de tas . Tout cela a un effet négatif sur les performances: cela implique le hachage et la comparaison des chaînes , la synchronisation pour l'accès aux données globales et l'accès répété à la mémoire de tas qui joue mal avec la mise en cache du processeur. De plus, les liaisons de variables dynamiques doivent être restaurées à leur variable précédente à la fin de let
, ce qui ajoute n
des recherches supplémentaires pour chaque let
bloc avec des n
liaisons.
Si vous vous liez lexical-binding
à t
dans l'exemple ci-dessus, le code d'octet est quelque peu différent:
0 constant 10
1 constant message
2 stack-ref 1
3 call 1
4 return
Notez que varbind
et varref
sont complètement partis. La variable locale est simplement poussée sur la pile et désignée par un décalage constant via l' stack-ref
instruction. Essentiellement, la variable est liée et lue avec un temps constant , la lecture et l'écriture dans la mémoire de la pile , qui est entièrement locale et joue donc bien avec la concurrence et la mise en cache du processeur , et n'implique aucune chaîne .
En général, avec liaison lexicales de lookups variables locales (par exemple let
, setq
, etc.) ont beaucoup moins de complexité d'exécution et de la mémoire .
Cet exemple spécifique
Avec la liaison dynamique, chaque let entraîne une pénalité de performance, pour les raisons ci-dessus. Le plus permet, les liaisons de variables plus dynamiques.
En particulier, avec un élément supplémentaire let
dans le loop
corps, la variable liée devrait être restaurée à chaque itération de la boucle , en ajoutant une recherche de variable supplémentaire à chaque itération . Par conséquent, il est plus rapide de conserver la sortie du corps de la boucle, de sorte que la variable d'itération n'est réinitialisée qu'une seule fois , une fois la boucle terminée. Cependant, ce n'est pas particulièrement élégant, car la variable d'itération est liée bien avant qu'elle ne soit réellement requise.
Avec la reliure lexicale, les let
s sont bon marché. Notamment, un let
corps de boucle n'est pas pire (en termes de performances) qu'un let
extérieur de corps de boucle. Par conséquent, il est parfaitement possible de lier des variables aussi localement que possible et de conserver la variable d'itération confinée au corps de la boucle.
Il est également légèrement plus rapide, car il compile beaucoup moins d'instructions. Considérez le démontage côte à côte suivi (let local sur le côté droit):
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
Je n'ai cependant aucune idée de ce qui cause la différence.
varbind
de code compilé sous liaison lexicale. C'est tout le but et le but.;; -*- lexical-binding: t -*-
, chargé et appelé(byte-compile 'sum1)
, en supposant que produit une définition compilée sous liaison lexicale. Cependant, cela ne semble pas l'avoir fait.byte-compile
avec le tampon correspondant en cours, ce qui est - soit dit en passant - exactement ce que fait le compilateur d'octets. Si vous invoquezbyte-compile
séparément, vous devez définir explicitementlexical-binding
, comme je l'ai fait dans ma réponse.