Enlace léxico versus enlace dinámico en general
Considere el siguiente ejemplo:
(let ((lexical-binding nil))
(disassemble
(byte-compile (lambda ()
(let ((foo 10))
(message foo))))))
Compila e inmediatamente desmonta un simple lambda
con una variable local. Con lexical-binding
deshabilitado, como arriba, el código de bytes se ve de la siguiente manera:
0 constant 10
1 varbind foo
2 constant message
3 varref foo
4 call 1
5 unbind 1
6 return
Tenga en cuenta las instrucciones varbind
y varref
. Estas instrucciones enlazan y buscan variables respectivamente por su nombre en un entorno de enlace global en la memoria de almacenamiento dinámico . Todo esto tiene un efecto adverso en el rendimiento: implica el hash y la comparación de cadenas , la sincronización para el acceso a datos globales y el acceso repetido a la memoria de almacenamiento dinámico que juega mal con el almacenamiento en caché de la CPU. Además, los enlaces de variables dinámicas deben restaurarse a su variable anterior al final de let
, lo que agrega n
búsquedas adicionales para cada let
bloque con n
enlaces.
Si se une lexical-binding
al t
ejemplo anterior, el código de bytes se ve algo diferente:
0 constant 10
1 constant message
2 stack-ref 1
3 call 1
4 return
Tenga en cuenta que varbind
y varref
se han ido por completo. La variable local simplemente se inserta en la pila y se hace referencia a ella mediante un desplazamiento constante a través de la stack-ref
instrucción. Esencialmente, la variable está vinculada y se lee con tiempo constante , lecturas y escrituras en la memoria en la pila , que es completamente local y, por lo tanto, juega bien con la concurrencia y el almacenamiento en caché de la CPU , y no involucra ninguna secuencia.
Generalmente, con las búsquedas de enlace léxico de variables locales (p let
. Ej . setq
, Etc.) tienen mucho menos tiempo de ejecución y complejidad de memoria .
Este ejemplo especifico
Con enlace dinámico, cada let incurre en una penalización de rendimiento, por las razones anteriores. Cuantos más let, más enlaces variables dinámicos.
Notablemente, con un adicional let
dentro del loop
cuerpo, la variable vinculada necesitaría ser restaurada en cada iteración del ciclo , agregando una búsqueda de variable adicional a cada iteración . Por lo tanto, es más rápido mantener la salida del cuerpo del bucle, de modo que la variable de iteración solo se restablezca una vez , después de que finalice todo el bucle. Sin embargo, esto no es particularmente elegante, ya que la variable de iteración está vinculada mucho antes de que sea realmente necesaria.
Con encuadernación léxica, los let
s son baratos. En particular, un let
cuerpo dentro de un bucle no es peor (en términos de rendimiento) que un let
exterior de un cuerpo de bucle. Por lo tanto, está perfectamente bien vincular variables lo más localmente posible y mantener la variable de iteración confinada al cuerpo del bucle.
También es un poco más rápido, ya que compila muchas menos instrucciones. Considere el siguiente desmontaje lado a lado (alquiler local en el lado derecho):
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
Sin embargo, no tengo idea de qué está causando la diferencia.
varbind
código compilado bajo enlace léxico. Ese es todo el punto y el propósito.