レキシカルスコープで「let」が高速になるのはなぜですか?


31

dolistマクロのソースコードを読んでいると、次のコメントに出会いました。

;; これは信頼できるテストではありませんが、両方のセマンティクスが許容されるため、重要ではありません。一方は動的スコープでわずかに速く、もう一方はレキシカルスコープでわずかに高速です(そしてより明確なセマンティクスを持ちます)

これはこのスニペットを参照しています(わかりやすくするために簡略化しています)。

(if lexical-binding
    (let ((temp list))
      (while temp
        (let ((it (car temp)))
          ;; Body goes here
          (setq temp (cdr temp)))))
  (let ((temp list)
        it)
    (while temp
      (setq it (car temp))
      ;; Body goes here
      (setq temp (cdr temp)))))

letループ内で使用されているフォームを見て驚いた。以前setqは、同じ外部変数で繰り返し使用するのに比べて遅いと考えていました(上記の2番目のケースで行われます)。

すぐ上のコメントについては、それが代替よりも高速であると明示的に言っている(レキシカルバインディングを使用している)場合を除いて、私はそれを何もないとして却下したでしょう。だから...それはなぜですか?

  1. 上記のコードは、字句バインディングと動的バインディングのパフォーマンスが異なるのはなぜですか?
  2. let字句を使用するとフォームが高速になるのはなぜですか?

回答:


38

一般的な字句バインディングと動的バインディング

次の例を考えてみましょう。

(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    

varbindvarref手順に注意してください。これらの命令は、ヒープメモリ上のグローバルバインディング環境で、それぞれの名前で変数をバインドおよびルックアップします。これはすべて、パフォーマンスに悪影響を及ぼします。文字列のハッシュと比較、グローバルデータアクセスの同期、およびCPUキャッシュと相性の悪い繰り返しのヒープメモリアクセスが含まれます。また、動的変数バインディングは、の終わりに以前の変数に復元する必要があります。これにより、バインディングを持つ各ブロックのルックアップが追加されます。letnletn

上記の例でバインドlexical-bindingする場合t、バイトコードは多少異なります。

0       constant  10
1       constant  message
2       stack-ref 1
3       call      1
4       return    

varbindvarrefは完全になくなっていることに注意してください。ローカル変数は単にスタックにプッシュされ、stack-ref命令を介して定数オフセットによって参照されます。基本的に、変数は定数時間でバインドおよび読み取りされ、スタック内メモリの読み取りおよび書き込みは完全にローカルであるため、同時実行性およびCPUキャッシングで適切に機能し、文字列は一切関与しません。

一般に、ローカル変数(たとえば、など)の字句バインディング検索でletsetq実行時間とメモリの複雑さはるかに少なくなります

この具体例

動的バインディングを使用すると、上記の理由により、各letでパフォーマンスが低下します。より多くの、より多くの動的変数バインディング。

特に、本体let内に追加loopがある場合、ループの各反復でバインドされた変数を復元し、各反復に追加の変数ルックアップを追加する必要があります。したがって、ループ全体を終了した後、反復変数が1回だけリセットされるように、ループ本体から抜け出すのが速いです。ただし、反復変数は実際に必要になる前にバインドされているため、これは特にエレガントではありません。

字句バインディングでは、letsは安価です。特に、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    

しかし、何が違いを引き起こしているのか、私には見当もつかない。


7

要するに、動的バインディングは非常に遅いです。字句バインディングは、実行時に非常に高速です。基本的な理由は、レキシカルバインディングはコンパイル時に解決できますが、動的バインディングは解決できないからです。

次のコードを検討してください。

(let ((x 42))
    (foo)
    (message "%d" x))

をコンパイルするときlet、コンパイラfooは(動的にバインドされた)変数xにアクセスするかどうかを知ることができないため、のバインディングを作成しx、変数の名前を保持する必要があります。字句バインディングを使用すると、コンパイラは名前なしでバインディングスタックのをダンプしx、正しいエントリに直接アクセスします。

しかし、待ってください-さらにあります。字句バインディングを使用すると、コンパイラは、この特定のバインディングがxコードでのみ使用されることを検証できmessageます。以来、x変更されることはありません、それはインラインに安全であるxと収量

(progn
  (foo)
  (message "%d" 42))

現在のバイトコードコンパイラがこの最適化を実行するとは思いませんが、今後もそうなると確信しています。

要するに:

  • 動的バインディングは、最適化の機会をほとんど許可しない重い操作です。
  • 字句バインディングは軽量の操作です。
  • 読み取り専用値の字句バインディングは、多くの場合最適化されます。

3

このコメントは、字句バインディングが動的バインディングよりも速いことも遅いことも示唆していません。むしろ、それらの異なる形式は、字句的および動的な結合の下で異なる性能特性を有することを示唆している。

だから、あるレキシカルスコープは速く動的スコープよりも?この場合、大きな違いはないと思われますが、わかりません。実際に測定する必要があります。


1
varbind字句バインディングの下で​​コンパイルされたコードにはありません。それが全体のポイントと目的です。
lunaryorn

うーん で始まる上記のソースを含むファイルを作成し;; -*- lexical-binding: t -*-、それをロードし、を呼び出して(byte-compile 'sum1)、字句バインディングの下で​​コンパイルされた定義を作成したと仮定します。しかし、そうではないようです。
gsg

誤った仮定に基づいていたため、バイトコードのコメントを削除しました。
gsg

lunaryonの答えは、このコード字句バインディングの方が明らか高速であることを示しています(もちろん、ミクロレベルでのみ)。
shosti

@gsgこの宣言は単なる標準のファイル変数であり、対応するファイルバッファーの外部から呼び出された関数には影響しません。IOW、ソースファイルにアクセスbyte-compileし、対応するバッファーを現在の状態で呼び出した場合にのみ効果があります。byte-compile個別に呼び出す場合lexical-bindingは、回答で行ったように、明示的に設定する必要があります。
lunaryorn 14年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.