Mengapa `membiarkan 'lebih cepat dengan ruang lingkup leksikal?

31

Saat membaca kode sumber untuk dolistmakro, saya berlari ke komentar berikut.

;; Ini bukan tes yang dapat diandalkan, tetapi tidak masalah karena kedua semantik dapat diterima, yang satu sedikit lebih cepat dengan pelingkupan dinamis dan yang lain sedikit lebih cepat (dan memiliki semantik yang lebih bersih) dengan pelingkupan leksikal .

Yang merujuk pada cuplikan ini (yang saya sederhanakan untuk kejelasan).

(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)))))

Itu mengejutkan saya melihat letbentuk yang digunakan di dalam lingkaran. Saya dulu berpikir itu lambat dibandingkan berulang kali menggunakan setqpada variabel eksternal yang sama (seperti yang dilakukan pada kasus kedua di atas).

Saya akan menampik itu sebagai apa-apa, kalau bukan karena komentar segera di atasnya secara eksplisit mengatakan itu lebih cepat daripada alternatif (dengan leksikal mengikat). Jadi ... Kenapa begitu?

  1. Mengapa kode di atas berbeda dalam kinerja pada pengikatan leksikal vs dinamis?
  2. Mengapa letformulir lebih cepat dengan leksikal?
Malabarba
sumber

Jawaban:

38

Mengikat leksikal versus pengikatan dinamis secara umum

Perhatikan contoh berikut:

(let ((lexical-binding nil))
  (disassemble
   (byte-compile (lambda ()
                   (let ((foo 10))
                     (message foo))))))

Ini mengkompilasi dan segera membongkar sederhana lambdadengan variabel lokal. Dengan lexical-bindingdinonaktifkan, seperti di atas, kode byte terlihat sebagai berikut:

0       constant  10
1       varbind   foo
2       constant  message
3       varref    foo
4       call      1
5       unbind    1
6       return    

Perhatikan varbinddan varrefinstruksinya. Instruksi ini mengikat dan mencari masing-masing variabel dengan namanya di lingkungan pengikatan global pada memori tumpukan . Semua ini memiliki efek buruk pada kinerja: Ini melibatkan string hashing dan perbandingan , sinkronisasi untuk akses data global, dan akses memori tumpukan berulang yang bermain buruk dengan caching CPU. Juga, binding variabel dinamis perlu dikembalikan ke variabel sebelumnya di akhir let, yang menambahkan npencarian tambahan untuk setiap letblok dengan nbinding.

Jika Anda mengikat lexical-bindinguntuk tdi contoh di atas, kode byte terlihat agak berbeda:

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

Catat itu varbinddan varrefsepenuhnya hilang. Variabel lokal hanya didorong ke stack, dan disebut dengan offset konstan melalui stack-refinstruksi. Pada dasarnya, variabel terikat dan dibaca dengan waktu konstan , memori in-stack membaca dan menulis, yang sepenuhnya lokal dan dengan demikian bermain baik dengan konkurensi dan caching CPU , dan tidak melibatkan string sama sekali.

Umumnya, dengan pencarian mengikat leksikal variabel lokal (misalnya let, setq, dll) memiliki lebih sedikit runtime dan memori kompleksitas .

Contoh spesifik ini

Dengan pengikatan dinamis, masing-masing membiarkan dikenakan penalti kinerja, untuk alasan di atas. Semakin banyak memungkinkan, binding variabel lebih dinamis.

Khususnya, dengan tambahan letdi dalam looptubuh, variabel terikat perlu dikembalikan pada setiap iterasi dari loop , menambahkan pencarian variabel tambahan untuk setiap iterasi . Oleh karena itu, lebih cepat untuk menjaga keluar dari tubuh loop, sehingga variabel iterasi hanya direset sekali , setelah seluruh loop selesai. Namun, ini tidak terlalu elegan, karena variabel iterasi terikat sebelum benar-benar diperlukan.

Dengan pengikatan leksikal, lets itu murah. Khususnya, di letdalam tubuh loop tidak lebih buruk (kinerja-bijaksana) daripada di letluar tubuh loop. Oleh karena itu, sangat baik untuk mengikat variabel secara lokal mungkin, dan menjaga variabel iterasi terbatas pada badan loop.

Ini juga sedikit lebih cepat, karena mengkompilasi instruksi jauh lebih sedikit. Pertimbangkan pembongkaran berdampingan yang diikuti (biarkan lokal di sisi kanan):

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    

Saya tidak tahu, apa yang menyebabkan perbedaan.

lunaryorn
sumber
7

Singkatnya - pengikatan dinamis sangat lambat. Pengikatan leksikal sangat cepat saat runtime. Alasan mendasarnya adalah bahwa pengikatan leksikal dapat diselesaikan pada waktu kompilasi, sementara pengikatan dinamis tidak dapat diselesaikan.

Pertimbangkan kode berikut:

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

Ketika mengkompilasinya let, kompiler tidak dapat mengetahui apakah fooakan mengakses variabel (terikat secara dinamis) x, jadi ia harus membuat pengikatan untuk x, dan harus mempertahankan nama variabel. Dengan mengikat leksikal, compiler hanya dump nilai dari xpada binding menumpuk, tanpa nama, dan mengakses entri yang tepat langsung.

Tapi tunggu - masih ada lagi. Dengan pengikatan leksikal, kompiler dapat memverifikasi bahwa pengikatan khusus xini hanya digunakan dalam kode untuk message; karena xtidak pernah dimodifikasi, aman untuk berbaris xdan menghasilkan

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

Saya tidak berpikir bahwa kompiler bytecode saat ini melakukan optimasi ini, tetapi saya yakin itu akan melakukannya di masa depan.

Singkatnya:

  • pengikatan dinamis adalah operasi berat yang memungkinkan beberapa peluang untuk optimisasi;
  • Ikatan leksikal adalah operasi yang ringan;
  • Ikatan leksikal dari nilai hanya baca seringkali dapat dioptimalkan.
jch
sumber
3

Komentar ini tidak menyarankan bahwa pengikatan leksikal lebih cepat atau lebih lambat dari pengikatan dinamis. Sebaliknya, ini menunjukkan bahwa bentuk-bentuk yang berbeda memiliki karakteristik kinerja yang berbeda di bawah pengikatan leksikal dan dinamis, misalnya, salah satu dari mereka lebih disukai di bawah satu disiplin yang mengikat, dan yang lainnya lebih disukai di yang lain.

Jadi apakah ruang lingkup leksikal lebih cepat daripada ruang lingkup dinamis? Saya menduga bahwa dalam hal ini tidak ada banyak perbedaan, tetapi saya tidak tahu - Anda benar-benar harus mengukurnya.

gsg
sumber
1
Tidak ada varbindkode yang dikompilasi di bawah pengikatan leksikal. Itulah inti dan tujuannya.
lunaryorn
Hmm. Saya membuat file yang berisi sumber di atas, dimulai dengan ;; -*- lexical-binding: t -*-, memuatnya, dan memanggil (byte-compile 'sum1), dengan asumsi bahwa menghasilkan definisi yang dikompilasi di bawah pengikatan leksikal. Namun, sepertinya tidak ada.
gsg
Menghapus komentar kode byte karena didasarkan pada asumsi yang salah.
gsg
lunaryon ini jawaban menunjukkan bahwa kode ini jelas adalah lebih cepat di bawah leksikal mengikat (meskipun tentu saja hanya pada tingkat mikro).
shosti
@ gsg Deklarasi ini hanya variabel file standar, yang tidak berpengaruh pada fungsi yang dipanggil dari luar buffer file yang sesuai. TKI, itu hanya memiliki efek jika Anda mengunjungi file sumber dan kemudian memanggil byte-compiledengan buffer yang sesuai saat ini, yang — omong-omong — persis apa yang dilakukan oleh byte compiler. Jika Anda memohon byte-compilesecara terpisah, Anda perlu mengatur secara eksplisit lexical-binding, seperti yang saya lakukan dalam jawaban saya.
lunaryorn