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