Oprawa leksykalna a oprawa dynamiczna w ogóle
Rozważ następujący przykład:
(let ((lexical-binding nil))
(disassemble
(byte-compile (lambda ()
(let ((foo 10))
(message foo))))))
Kompiluje i natychmiast dezasembluje prosty lambda
ze zmienną lokalną. Po lexical-binding
wyłączeniu, jak wyżej, kod bajtu wygląda następująco:
0 constant 10
1 varbind foo
2 constant message
3 varref foo
4 call 1
5 unbind 1
6 return
Zwróć uwagę na instrukcje varbind
i varref
. Instrukcje te wiążą i wyszukują odpowiednio zmienne według ich nazwy w globalnym środowisku powiązań w pamięci sterty . Wszystko to ma negatywny wpływ na wydajność: obejmuje haszowanie i porównywanie ciągów , synchronizację globalnego dostępu do danych oraz powtarzający się dostęp do pamięci sterty, który źle działa przy buforowaniu procesora. Ponadto powiązania zmiennych dynamicznych należy przywrócić do ich poprzedniej zmiennej na końcu let
, co dodaje n
dodatkowe wyszukiwania dla każdego let
bloku z n
powiązaniami.
Jeśli wiążą lexical-binding
się t
w powyższym przykładzie, kod bajtowy wygląda nieco inaczej:
0 constant 10
1 constant message
2 stack-ref 1
3 call 1
4 return
Zauważ, że varbind
i varref
całkowicie zniknęły. Zmienna lokalna jest po prostu wypychana na stos i określana przez stałe przesunięcie za pomocą stack-ref
instrukcji. Zasadniczo zmienna jest związana i odczytywana ze stałym czasem , pamięć na stosie odczytuje i zapisuje, co jest całkowicie lokalne, a zatem dobrze gra z buforowaniem współbieżności i procesora , i nie wymaga żadnych łańcuchów.
Generalnie, z leksykalnych wiążących wyszukiwań zmiennych lokalnych (np let
, setq
etc.) mają znacznie mniejszą złożoność wykonania i pamięci .
Ten konkretny przykład
Z dynamicznym wiązaniem każdy z nich ponosi karę wydajnościową z powyższych powodów. Im więcej pozwala, tym bardziej dynamiczne wiązania zmiennych.
W szczególności, z dodatkową let
częścią loop
ciała, powiązana zmienna musiałaby być przywracana przy każdej iteracji pętli , dodając dodatkowe wyszukiwanie zmiennych do każdej iteracji . Dlatego szybsze jest utrzymanie wypuszczenia z ciała pętli, tak że zmienna iteracji jest resetowana tylko raz , po zakończeniu całej pętli. Nie jest to jednak szczególnie eleganckie, ponieważ zmienna iteracyjna jest związana, zanim faktycznie będzie wymagana.
W przypadku leksykalnego wiązania let
s są tanie. W szczególności, let
ciało wewnątrz pętli nie jest gorsze (pod względem wydajności) niż let
ciało zewnętrzne pętli. Dlatego idealnie można powiązać zmienne tak lokalnie, jak to możliwe, i zachować zmienną iteracyjną ograniczoną do ciała pętli.
Jest również nieco szybszy, ponieważ kompiluje się do znacznie mniej instrukcji. Rozważ następujący demontaż side-by-side (lokalny let po prawej stronie):
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
Nie mam jednak pojęcia, co powoduje różnicę.
varbind
kompilacji leksykalnej nie ma kodu skompilowanego. Taki jest cały cel i cel.