Ligação lexical versus ligação dinâmica em geral
Considere o seguinte exemplo:
(let ((lexical-binding nil))
(disassemble
(byte-compile (lambda ()
(let ((foo 10))
(message foo))))))
Compila e desmonta imediatamente um simples lambda
com uma variável local. Com lexical-binding
desativado, como acima, o código de bytes é o seguinte:
0 constant 10
1 varbind foo
2 constant message
3 varref foo
4 call 1
5 unbind 1
6 return
Observe as instruções varbind
e varref
. Essas instruções ligam e pesquisam as variáveis, respectivamente, pelo nome em um ambiente de ligação global na memória heap . Tudo isso tem um efeito adverso no desempenho: envolve hash e comparação de strings , sincronização para acesso global a dados e acesso repetido à memória heap, que é prejudicial ao cache da CPU. Além disso, as ligações de variáveis dinâmicas precisam ser restauradas para sua variável anterior no final de let
, o que adiciona n
pesquisas adicionais para cada let
bloco com n
ligações.
Se você vincular lexical-binding
a t
no exemplo acima, o código byte parece um pouco diferente:
0 constant 10
1 constant message
2 stack-ref 1
3 call 1
4 return
Note que varbind
e se varref
foram completamente. A variável local é simplesmente empurrada para a pilha e referida por um deslocamento constante através da stack-ref
instrução. Essencialmente, a variável é vinculada e lida com tempo constante , as leituras e gravações de memória na pilha , que são inteiramente locais e, portanto, funcionam bem com simultaneidade e cache de CPU , e não envolvem nenhuma string.
Geralmente, com pesquisas de ligação lexicais de variáveis locais (por exemplo let
, setq
, etc.) têm muito menos tempo de execução e memória complexidade .
Este exemplo específico
Com ligação dinâmica, cada uma delas incorre em uma penalidade de desempenho, pelas razões acima. Quanto mais vamos permitir, mais vinculações de variáveis dinâmicas.
Notavelmente, com um adicional let
dentro do loop
corpo, a variável vinculada precisaria ser restaurada a cada iteração do loop , adicionando uma pesquisa de variável adicional a cada iteração . Portanto, é mais rápido manter a liberação do corpo do loop, para que a variável de iteração seja redefinida apenas uma vez , após o término de todo o loop. No entanto, isso não é particularmente elegante, pois a variável de iteração é vinculada muito antes de ser realmente necessária.
Com encadernação lexical, let
s são baratos. Notavelmente, um let
corpo dentro de um loop não é pior (em termos de desempenho) do que o let
exterior de um corpo de loop. Portanto, é perfeitamente bom vincular variáveis o mais local possível e manter a variável de iteração confinada ao corpo do loop.
Também é um pouco mais rápido, porque compila com muito menos instruções. Considere a desmontagem seguida a lado (local deixe no lado direito):
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
Não tenho idéia, no entanto, o que está causando a diferença.
varbind
código compilado sob ligação lexical. Esse é o objetivo e o objetivo.;; -*- lexical-binding: t -*-
, carregado e chamado(byte-compile 'sum1)
, assumindo que produzisse uma definição compilada sob ligação lexical. No entanto, parece não ter.byte-compile
com o buffer correspondente atual, que é - a propósito - exatamente o que o compilador de bytes está fazendo. Se você chamarbyte-compile
separadamente, precisará definir explicitamentelexical-binding
, como eu fiz na minha resposta.