Tại sao 'hãy' nhanh hơn với phạm vi từ vựng?


31

Trong khi đọc qua mã nguồn cho dolistmacro, tôi đã chạy vào bình luận sau đây.

;; Đây không phải là một bài kiểm tra đáng tin cậy, nhưng nó không quan trọng bởi vì cả hai ngữ nghĩa đều được chấp nhận, tho một là nhanh hơn một chút với phạm vi động và cái kia nhanh hơn một chút (và có ngữ nghĩa sạch hơn) với phạm vi từ vựng .

Mà đề cập đến đoạn trích này (mà tôi đã đơn giản hóa cho rõ ràng).

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

Nó làm tôi ngạc nhiên khi thấy một lethình thức được sử dụng trong một vòng lặp. Tôi đã từng nghĩ rằng nó chậm so với việc sử dụng nhiều lần setqtrên cùng một biến ngoài (như được thực hiện trong trường hợp thứ hai ở trên).

Tôi sẽ bỏ qua rằng không có gì, nếu không cho nhận xét ngay phía trên nó nói rõ ràng là nhanh hơn thay thế (với ràng buộc từ vựng). Vậy ... Tại sao vậy?

  1. Tại sao mã ở trên khác nhau về hiệu suất trên liên kết từ vựng và động?
  2. Tại sao lethình thức nhanh hơn với từ vựng?

Câu trả lời:


38

Liên kết từ điển so với ràng buộc động nói chung

Hãy xem xét ví dụ sau:

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

Nó biên dịch và ngay lập tức phân tách một đơn giản lambdavới một biến cục bộ. Với lexical-bindingvô hiệu hóa, như trên, mã byte trông như sau:

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

Lưu ý varbindvarrefhướng dẫn. Các hướng dẫn này liên kết và tra cứu các biến tương ứng theo tên của chúng trong môi trường liên kết toàn cầu trên bộ nhớ heap . Tất cả điều này có ảnh hưởng xấu đến hiệu suất: Nó bao gồm băm và so sánh chuỗi , đồng bộ hóa để truy cập dữ liệu toàn cầu và truy cập bộ nhớ heap lặp đi lặp lại , điều này rất tệ với bộ nhớ đệm CPU. Ngoài ra, các ràng buộc biến động cần phải được khôi phục về biến trước đó của chúng ở cuối let, điều này bổ nsung thêm tra cứu cho mỗi letkhối với ncác ràng buộc.

Nếu bạn ràng buộc lexical-bindingđể ttrong ví dụ trên, các mã byte trông hơi khác:

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

Lưu ý rằng varbindvarrefhoàn toàn biến mất. Biến cục bộ chỉ đơn giản được đẩy lên ngăn xếp và được gọi bằng một giá trị bù không đổi thông qua stack-refhướng dẫn. Về cơ bản, biến bị ràng buộc và đọc với thời gian không đổi , bộ nhớ trong ngăn xếp đọc và ghi, hoàn toàn cục bộ và do đó chơi tốt với bộ đệm đồng thời và bộ đệm CPU , và không liên quan đến bất kỳ chuỗi nào.

Nói chung, với tra cứu từ vựng ràng buộc của các biến địa phương (ví dụ let, setqvv) có ít thời gian chạy và bộ nhớ phức tạp .

Ví dụ cụ thể này

Với ràng buộc động, mỗi người sẽ phải chịu một hình phạt hiệu suất, vì những lý do trên. Càng cho phép, các ràng buộc biến động càng nhiều.

Đáng chú ý, với một bổ sung lettrong loopcơ thể, biến bị ràng buộc sẽ cần phải được khôi phục ở mỗi lần lặp của vòng lặp , thêm một tra cứu biến bổ sung cho mỗi lần lặp . Do đó, nhanh hơn để giữ cho phép thoát khỏi thân vòng lặp, do đó biến lặp chỉ được đặt lại một lần , sau khi toàn bộ vòng lặp kết thúc. Tuy nhiên, điều này không đặc biệt tao nhã, vì biến lặp được ràng buộc theo cách trước khi nó thực sự được yêu cầu.

Với ràng buộc từ vựng, lets là giá rẻ. Đáng chú ý, một thân lettrong vòng lặp không tệ hơn (hiệu năng thông minh) so với letbên ngoài thân vòng lặp. Do đó, việc liên kết các biến cục bộ càng tốt càng tốt và giữ biến lặp được giới hạn trong thân vòng lặp.

Nó cũng nhanh hơn một chút, vì nó biên dịch theo hướng dẫn ít hơn nhiều. Xem xét việc tháo gỡ bên cạnh theo sau (cục bộ ở bên phải):

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    

Tôi không có manh mối, mặc dù, những gì gây ra sự khác biệt.


7

Trong ngắn hạn - ràng buộc năng động là rất chậm. Liên kết từ vựng là cực kỳ nhanh chóng trong thời gian chạy. Lý do cơ bản là ràng buộc từ vựng có thể được giải quyết tại thời điểm biên dịch, trong khi ràng buộc động không thể.

Hãy xem xét các mã sau đây:

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

Khi biên dịch nó let, trình biên dịch không thể biết liệu có chấp foonhận biến (ràng buộc động) hay không x, do đó nó phải tạo ra một ràng buộc cho xvà phải giữ lại tên của biến. Với liên kết từ vựng, trình biên dịch chỉ cần bỏ giá trị của xngăn xếp liên kết, không có tên của nó và truy cập trực tiếp vào mục bên phải.

Nhưng xin chờ chút nữa. Với ràng buộc từ vựng, trình biên dịch có thể xác minh rằng ràng buộc cụ thể xnày chỉ được sử dụng trong mã để message; vì xkhông bao giờ được sửa đổi, nó an toàn cho nội tuyến xvà năng suất

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

Tôi không nghĩ rằng trình biên dịch mã byte hiện tại thực hiện tối ưu hóa này, nhưng tôi tin tưởng rằng nó sẽ làm như vậy trong tương lai.

Vì vậy, trong ngắn hạn:

  • ràng buộc động là một hoạt động có trọng lượng lớn cho phép ít cơ hội để tối ưu hóa;
  • ràng buộc từ vựng là một hoạt động nhẹ;
  • ràng buộc từ vựng của một giá trị chỉ đọc thường có thể được tối ưu hóa đi.

3

Nhận xét này không cho thấy ràng buộc từ vựng là nhanh hơn hoặc chậm hơn ràng buộc động. Thay vào đó, nó gợi ý rằng các hình thức khác nhau đó có các đặc tính hiệu suất khác nhau theo ràng buộc từ vựng và động, ví dụ: một trong số chúng thích hợp hơn theo một môn học ràng buộc và các dạng khác thích hợp hơn trong các môn học khác.

Vì vậy, phạm vi từ vựng nhanh hơn so với phạm vi năng động? Tôi nghi ngờ rằng trong trường hợp này không có nhiều khác biệt, nhưng tôi không biết - bạn thực sự phải đo nó.


1
Không có varbindmã được biên dịch theo ràng buộc từ vựng. Đó là toàn bộ quan điểm và mục đích.
lunaryorn

Hừm. Tôi đã tạo một tệp chứa nguồn ở trên, bắt đầu bằng ;; -*- lexical-binding: t -*-, tải nó và gọi (byte-compile 'sum1), giả sử rằng đã tạo ra một định nghĩa được biên dịch theo ràng buộc từ vựng. Tuy nhiên, nó dường như không có.
gsg

Đã xóa các nhận xét mã byte vì chúng dựa trên giả định sai đó.
gsg

Câu trả lời của lunaryon cho thấy rõ ràng mã này nhanh hơn theo ràng buộc từ vựng (mặc dù tất nhiên chỉ ở cấp vi mô).
shosti

@gsg Khai báo này chỉ là một biến tệp tiêu chuẩn, không có tác dụng đối với các hàm được gọi từ bên ngoài bộ đệm tệp tương ứng. IOW, nó chỉ có hiệu lực nếu bạn truy cập vào tệp nguồn và sau đó gọi byte-compilevới bộ đệm tương ứng là dòng điện, đó là bằng cách cách chính xác các trình biên dịch byte đang làm gì. Nếu bạn gọi byte-compileriêng, bạn cần đặt rõ ràng lexical-binding, như tôi đã làm trong câu trả lời của mình.
lunaryorn
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.