一般的な字句バインディングと動的バインディング
次の例を考えてみましょう。
(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
手順に注意してください。これらの命令は、ヒープメモリ上のグローバルバインディング環境で、それぞれの名前で変数をバインドおよびルックアップします。これはすべて、パフォーマンスに悪影響を及ぼします。文字列のハッシュと比較、グローバルデータアクセスの同期、およびCPUキャッシュと相性の悪い繰り返しのヒープメモリアクセスが含まれます。また、動的変数バインディングは、の終わりに以前の変数に復元する必要があります。これにより、バインディングを持つ各ブロックのルックアップが追加されます。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
命令を介して定数オフセットによって参照されます。基本的に、変数は定数時間でバインドおよび読み取りされ、スタック内メモリの読み取りおよび書き込みは完全にローカルであるため、同時実行性およびCPUキャッシングで適切に機能し、文字列は一切関与しません。
一般に、ローカル変数(たとえば、など)の字句バインディング検索でlet
はsetq
、実行時間とメモリの複雑さがはるかに少なくなります。
この具体例
動的バインディングを使用すると、上記の理由により、各letでパフォーマンスが低下します。より多くの、より多くの動的変数バインディング。
特に、本体let
内に追加loop
がある場合、ループの各反復でバインドされた変数を復元し、各反復に追加の変数ルックアップを追加する必要があります。したがって、ループ全体を終了した後、反復変数が1回だけリセットされるように、ループ本体から抜け出すのが速いです。ただし、反復変数は実際に必要になる前にバインドされているため、これは特にエレガントではありません。
字句バインディングでは、let
sは安価です。特に、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
字句バインディングの下でコンパイルされたコードにはありません。それが全体のポイントと目的です。