Lexical binding versus dynamic binding in general
Consider the following example:
(let ((lexical-binding nil))
(byte-compile (lambda ()
(let ((foo 10))
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
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
If you bind
t in the above example, the byte code looks somewhat different:
0 constant 10
1 constant message
2 stack-ref 1
3 call 1
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.
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,
lets 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
14 varset temp
15 goto-if-not-nil 1
18 constant nil
19:2 unbind 2
I have no clue, though, what's causing the difference.