Лексическое связывание и динамическое связывание в целом
Рассмотрим следующий пример:
(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
с дешевы. Примечательно, что 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
в коде, скомпилированном под лексической привязкой. В этом весь смысл и цель.