Lexical binding versus dynamic binding in general
Consider the following example:
(let ((lexical-binding nil))
(disassemble
(byte-compile '(lambda ()
(let ((foo 10))
(message foo)))))
It compiles and immediately disassembles a simple lambda
with a local variable. With lexical-binding
disabled, as above, the byte code looks as follows:
0 constant 10
1 varbind foo
2 constant message
3 varref foo
4 call 1
5 unbind 1
6 return
Note the varbind
and varref
instructions. These instructions bind and lookup respectively variables by their name in a global binding environment on heap memory. All this has an adverse effect on performance: It involves string hashing and comparison, synchronisation for global data access, and repeated heap memory access which plays badly with CPU caching. Also, dynamic variable bindings need to be restored to their previous variable at the end of let
, which adds n
additional lookups for each let
block with n
bindings.
If you bind lexical-binding
to t
in the above example, the byte code looks somewhat different:
0 constant 10
1 constant message
2 stack-ref 1
3 call 1
4 return
Note that varbind
and varref
are entirely gone. The local variable is simply pushed onto the stack, and referred to by a constant offset via the stack-ref
instruction. Essentially, the variable is bound and read with constant time, in-stack memory reads and writes, which is entirely local and thus plays well with concurrency and CPU caching, and does not involve any strings at all.
Generally, with lexical binding lookups of local variables (e.g. let
, setq
, etc.) have much less runtime and memory complexity.
This specific example
With dynamical binding, each let incurs a performance penalty, for above reasons. The more lets, the more dynamic variable bindings.
Notably, with an additional let
within the loop
body, the bound variable would need to be restored at every iteration of the loop, adding an additional variable lookup to each iteration. Hence, it's faster to keep the let out of the loop body, so that the iteration variable is only reset once, after the whole loop finished. However, this is not particularly elegant, since the iteration variable is bound way before it's actually required.
With lexical binding, let
s are cheap. Notably, a let
within a loop body is not worse (performance-wise) than a let
outside of a loop body. Hence, it's perfectly fine to bind variables as locally as possible, and keep the iteration variable confined to the loop body.
It's also slightly faster, because it compiles to much less instructions. Consider the followed side-by-side disassembly (local let on the right side):
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
I have no clue, though, what's causing the difference.