Лексичне зв’язування проти динамічного зв’язування загалом
Розглянемо наступний приклад:
(let ((lexical-binding nil))
(disassemble
(byte-compile (lambda ()
(let ((foo 10))
(message foo))))))
Він збирає та негайно розбирає просту lambda
з локальною змінною. Якщо lexical-binding
вимкнено, як і вище, код байти виглядає так:
0 constant 10
1 varbind foo
2 constant message
3 varref foo
4 call 1
5 unbind 1
6 return
Зверніть увагу varbind
та varref
інструкції. Ці вказівки пов'язують і шукають змінні відповідно змін за своїм ім'ям у глобальному середовищі прив'язки на купі пам'яті . Все це негативно позначається на продуктивності: воно включає хешування рядків та порівняння , синхронізацію для глобального доступу до даних та повторний доступ до пам’яті в купі, що погано грає з кешуванням процесора. Крім того, динамічні прив'язки змінної повинні бути відновлені до їх попередньої змінної в кінці let
, що додає n
додаткові пошуки для кожного let
блоку з n
прив’язками.
Якщо ви посилаєтесь lexical-binding
на t
наведений вище приклад, байт-код виглядає дещо інакше:
0 constant 10
1 constant message
2 stack-ref 1
3 call 1
4 return
Зауважте, що varbind
і varref
повністю пішли. Локальна змінна просто висувається на стек і посилається на постійне зміщення через stack-ref
інструкцію. По суті, змінна пов'язана і читається з постійним часом , в пам'яті стік читає і записує, що є повністю локальним і, таким чином, добре поєднується з одночасним кешуванням і кешуванням процесора , і зовсім не включає жодних рядків.
Як правило, з лексичними зв'язують пошуків локальних змінних (наприклад let
, setq
і т.д.) мають набагато менше часу виконання і пам'яті складності .
Цей конкретний приклад
Завдяки динамічній прив'язці кожен з них несе певну ефективність за вищезазначеними причинами. Чим більше літ, тим більш динамічні прив'язки змінних.
Зокрема, з додатковою частиною let
в loop
тілі пов'язану змінну потрібно буде відновити при кожній ітерації циклу , додаючи додатковий пошук змінної до кожної ітерації . Отже, швидше зберегти випущений з корпусу циклу, щоб змінна ітерація була скинута лише один раз , після того, як закінчиться весь цикл. Однак це не особливо елегантно, оскільки ітераційна змінна є обмеженою, перш ніж вона дійсно потрібна.
З лексичним зв’язуванням, let
s дешеві. Зокрема, let
всередині циклу циклу не гірше (продуктивне), ніж let
зовнішнє тіло циклу. Отже, ідеально добре пов'язувати змінні якомога локальніше і тримати змінну ітерації обмеженим тілом циклу.
Це також трохи швидше, тому що він збирає набагато менше інструкцій. Розгляньте наступні розбірки (місцевий пуск з правого боку):
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
У мене немає поняття, однак, що викликає різницю.
varbind
коду, складеного під лексичним прив'язкою. У цьому вся суть і мета.