ทำไม `ให้ 'เร็วขึ้นด้วยขอบเขตศัพท์?


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กับตัวแปรภายนอกเดียวกัน (เช่นเดียวกับที่ทำในกรณีที่สองด้านบน)

ฉันจะไม่สนใจสิ่งนั้นหากไม่ใช่เพราะความคิดเห็นในทันทีข้างต้นจะเป็นการบอกว่ามันเร็วกว่าทางเลือกอื่น (ด้วยการผูกคำ) ดังนั้น ... ทำไมจึงเป็นเช่นนั้น

  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    

หมายเหตุvarbindและvarrefคำแนะนำ คำแนะนำเหล่านี้ผูกและค้นหาตามลำดับตัวแปรโดยชื่อของพวกเขาในโลกสภาพแวดล้อมที่มีผลผูกพันในหน่วยความจำกอง ทั้งหมดนี้มีผลกระทบต่อประสิทธิภาพการทำงาน: มันเกี่ยวข้องกับการhashing สตริงและการเปรียบเทียบ , ประสานสำหรับการเข้าถึงข้อมูลทั่วโลกและทำซ้ำเข้าถึงหน่วยความจำกองที่เล่นไม่ดีกับแคช 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ภายในloopร่างกายตัวแปรที่ถูกผูกไว้จะต้องได้รับการบูรณะที่ซ้ำของวงทุก , เพิ่มการค้นหาตัวแปรเพิ่มเติมเพื่อให้แต่ละซ้ำ ดังนั้นจึงเป็นการเร็วกว่าที่จะป้องกันการปล่อยให้ออกจากร่างกายลูปเพื่อให้ตัวแปรการวนซ้ำถูกรีเซ็ตเพียงครั้งเดียวหลังจากการวนซ้ำทั้งหมดเสร็จสิ้น อย่างไรก็ตามนี่ไม่ได้สวยงามโดยเฉพาะเนื่องจากตัวแปรการวนซ้ำนั้นถูกผูกไว้ก่อนที่จะต้องใช้จริง

ด้วยการรวมคำศัพท์lets มีราคาถูก ยวด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จะ acess ตัวแปร (ถูกผูกมัดแบบไดนามิก) xดังนั้นมันจะต้องสร้างการเชื่อมโยงxและจะต้องรักษาชื่อของตัวแปร ด้วยการโยงคำศัพท์คอมไพเลอร์เพียงทิ้งค่าของxบนการผูกสแต็กโดยไม่มีชื่อและเข้าถึงรายการที่ถูกต้องโดยตรง

แต่เดี๋ยวก่อน - มีอีกมาก ด้วยการรวมศัพท์ศัพท์คอมไพเลอร์สามารถตรวจสอบได้ว่าการรวมเฉพาะนี้xถูกใช้ในรหัสไปmessageเท่านั้น เนื่องจากxไม่เคยถูกแก้ไขจึงปลอดภัยที่จะอินไลน์xและให้ผลผลิต

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

ฉันไม่คิดว่าคอมไพเลอร์ bytecode ปัจจุบันทำการปรับให้เหมาะสมนี้ แต่ฉันมั่นใจว่ามันจะทำในอนาคต

ดังนั้นในระยะสั้น:

  • การเชื่อมโยงแบบไดนามิกเป็นการดำเนินการที่มีน้ำหนักมาก
  • ศัพท์ผูกพันคือการดำเนินการที่มีน้ำหนักเบา;
  • ศัพท์ผูกพันของค่าอ่านอย่างเดียวมักจะสามารถเพิ่มประสิทธิภาพออกไป

3

ความคิดเห็นนี้ไม่ได้แนะนำว่าการโยงคำศัพท์จะเร็วหรือช้ากว่าการเชื่อมแบบไดนามิก ค่อนข้างแสดงให้เห็นว่ารูปแบบต่าง ๆ เหล่านั้นมีลักษณะการทำงานที่แตกต่างกันภายใต้การเชื่อมศัพท์และแบบไดนามิกเช่นหนึ่งในนั้นจะดีกว่าภายใต้วินัยผูกพันหนึ่งและอีกอันหนึ่งดีกว่าในอีกด้านหนึ่ง

ดังนั้นขอบเขตคำศัพท์จะเร็วกว่าขอบเขตไดนามิกหรือไม่ ฉันสงสัยว่าในกรณีนี้มีความแตกต่างไม่มาก แต่ฉันไม่รู้ - คุณต้องวัดมันจริงๆ


1
ไม่มีvarbindรหัสในการรวบรวมภายใต้การผูกคำ นั่นคือจุดรวมและวัตถุประสงค์
Lunaryorn

อืมมม ฉันสร้างไฟล์ที่มีซอร์สข้างต้นเริ่มต้น;; -*- lexical-binding: t -*-โหลดและเรียกใช้(byte-compile 'sum1)โดยสมมติว่าสร้างนิยามที่คอมไพล์ภายใต้การเชื่อมศัพท์ อย่างไรก็ตามดูเหมือนว่าจะไม่มี
gsg

ลบความคิดเห็นรหัสไบต์ตามที่พวกเขาคิดขึ้นมาเอง
gsg

lunaryon ของคำตอบที่แสดงให้เห็นว่ารหัสนี้ได้อย่างชัดเจนคือเร็วขึ้นภายใต้คำศัพท์ที่มีผลผูกพัน ( แต่แน่นอนเพียงในระดับจุลภาค)
shosti

@gsg การประกาศนี้เป็นเพียงตัวแปรไฟล์มาตรฐานซึ่งไม่มีผลกับฟังก์ชั่นที่เรียกใช้จากภายนอกบัฟเฟอร์ไฟล์ที่เกี่ยวข้อง IOW มันจะมีผลก็ต่อเมื่อคุณเยี่ยมชมไฟล์ต้นฉบับและจากนั้นเรียกใช้byte-compileบัฟเฟอร์ที่สอดคล้องกันว่าเป็นปัจจุบันซึ่งก็คือ - โดยวิธี - สิ่งที่ไบต์คอมไพเลอร์กำลังทำอยู่ หากคุณเรียกใช้byte-compileแยกต่างหากคุณจะต้องตั้งค่าอย่างชัดเจนlexical-bindingเช่นเดียวกับที่ฉันทำในคำตอบของฉัน
Lunaryorn
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.