ความเกียจคร้าน
ไม่ใช่ "การเพิ่มประสิทธิภาพคอมไพเลอร์" แต่เป็นสิ่งที่รับประกันโดยข้อกำหนดภาษาดังนั้นคุณจึงสามารถวางใจได้ว่าจะเกิดอะไรขึ้น โดยพื้นฐานแล้วนี่หมายความว่างานจะไม่ถูกดำเนินการจนกว่าคุณจะ "ทำอะไรบางอย่าง" กับผลลัพธ์ (เว้นเสียแต่ว่าคุณจะทำหลายอย่างเพื่อปิดความขี้เกียจ)
เห็นได้ชัดว่านี่เป็นหัวข้อทั้งหมดในสิทธิของตนเองและดังนั้นมีคำถามและคำตอบมากมายเกี่ยวกับเรื่องนี้แล้ว
จากประสบการณ์ที่ จำกัด ของฉันการทำให้โค้ดของคุณขี้เกียจเกินไปหรือเข้มงวดเกินไปมีบทลงโทษที่มีประสิทธิภาพอย่างมาก (ในเวลาและสถานที่) มากกว่าสิ่งอื่นใดที่ฉันกำลังจะพูดถึง ...
การวิเคราะห์ความเข้มงวด
ความเกียจคร้านเป็นเรื่องเกี่ยวกับการหลีกเลี่ยงงานเว้นแต่จำเป็น หากคอมไพเลอร์สามารถระบุได้ว่าผลลัพธ์ที่ต้องการจะ "เสมอ" เป็นสิ่งจำเป็นแล้วมันจะไม่รบกวนการจัดเก็บการคำนวณและดำเนินการในภายหลัง มันจะทำการแสดงโดยตรงเนื่องจากมีประสิทธิภาพมากขึ้น สิ่งนี้เรียกว่า "การวิเคราะห์ความเข้มงวด"
เห็นได้ชัดว่า gotcha คือคอมไพเลอร์ไม่สามารถตรวจพบได้ตลอดเวลาเมื่อมีบางสิ่งที่เข้มงวด บางครั้งคุณจำเป็นต้องให้คำแนะนำเพียงเล็กน้อยสำหรับคอมไพเลอร์ (ฉันไม่ทราบวิธีง่ายๆในการพิจารณาว่าการวิเคราะห์ความเข้มงวดได้ทำในสิ่งที่คุณคิดว่ามันมีหรือไม่นอกจากลุยผ่าน Core output)
การจัด
หากคุณเรียกใช้ฟังก์ชันและคอมไพเลอร์สามารถบอกได้ว่าคุณกำลังเรียกใช้ฟังก์ชันใดมันอาจลอง "inline" ฟังก์ชั่นนั้น - นั่นคือเพื่อแทนที่การเรียกใช้ฟังก์ชันด้วยสำเนาของฟังก์ชันเอง โอเวอร์เฮดของการเรียกใช้ฟังก์ชันมักจะค่อนข้างเล็ก แต่การอินไลน์มักทำให้เกิดการเพิ่มประสิทธิภาพอื่น ๆ ซึ่งจะไม่เกิดขึ้นเป็นอย่างอื่นดังนั้นการอินไลน์อาจเป็นชัยชนะครั้งใหญ่
ฟังก์ชั่นจะถูกขีดเส้นใต้หากพวกเขา "เล็กพอ" (หรือถ้าคุณเพิ่ม pragma โดยเฉพาะขออินไลน์) นอกจากนี้ยังสามารถใช้ฟังก์ชันอินไลน์ได้หากคอมไพเลอร์สามารถบอกได้ว่าคุณกำลังเรียกใช้ฟังก์ชันใด มีสองวิธีหลักที่คอมไพเลอร์ไม่สามารถบอกได้:
หากฟังก์ชั่นที่คุณโทรถูกส่งผ่านจากที่อื่น เช่นเมื่อfilter
รวบรวมฟังก์ชั่นคุณจะไม่สามารถอินไลน์เพรดิเคตกรองได้เนื่องจากเป็นอาร์กิวเมนต์ที่ผู้ใช้ระบุ
หากฟังก์ชั่นที่คุณโทรเป็นวิธีการเรียนและคอมไพเลอร์ไม่ทราบว่ามีส่วนเกี่ยวข้องประเภทใด เช่นเมื่อsum
รวบรวมฟังก์ชั่นคอมไพเลอร์ไม่สามารถอินไลน์+
ฟังก์ชั่นได้เพราะsum
ทำงานกับตัวเลขหลายประเภทซึ่งแต่ละอันมี+
ฟังก์ชั่นที่แตกต่างกัน
ในกรณีหลังคุณสามารถใช้{-# SPECIALIZE #-}
pragma เพื่อสร้างรุ่นของฟังก์ชั่นที่ฮาร์ดโค้ดให้เป็นประเภทเฉพาะ เช่น{-# SPECIALIZE sum :: [Int] -> Int #-}
จะคอมไพล์เวอร์ชันของsum
ฮาร์ดโค้ดสำหรับInt
ประเภทซึ่งหมายความว่า+
สามารถอินไลน์ในเวอร์ชันนี้
หมายเหตุ แต่ที่พิเศษใหม่ของเราฟังก์ชั่นจะถูกเรียกเมื่อคอมไพเลอร์สามารถบอกได้ว่าเรากำลังทำงานร่วมกับsum
Int
มิฉะนั้นsum
จะได้รับการเรียกว่าpolymorphic ดั้งเดิม อีกครั้งค่าใช้จ่ายในการเรียกใช้ฟังก์ชั่นจริงมีขนาดค่อนข้างเล็ก มันเป็นการเพิ่มประสิทธิภาพเพิ่มเติมที่อินไลน์สามารถเปิดใช้งานซึ่งเป็นประโยชน์
กำจัด subexpression ทั่วไป
หากบล็อกของโค้ดคำนวณค่าเดียวกันสองครั้งคอมไพเลอร์อาจแทนที่ด้วยอินสแตนซ์เดียวของการคำนวณเดียวกัน ตัวอย่างเช่นถ้าคุณทำ
(sum xs + 1) / (sum xs + 2)
คอมไพเลอร์อาจปรับให้เหมาะกับ
let s = sum xs in (s+1)/(s+2)
คุณอาจคาดหวังว่าคอมไพเลอร์จะเสมอทำเช่นนี้ อย่างไรก็ตามเห็นได้ชัดว่าในบางสถานการณ์อาจส่งผลให้ประสิทธิภาพแย่ลงไม่ดีขึ้นดังนั้น GHC จึงไม่ทำเช่นนี้เสมอไป ตรงไปตรงมาฉันไม่เข้าใจรายละเอียดที่อยู่เบื้องหลังอันนี้จริงๆ แต่ข้อดีคือถ้าการเปลี่ยนแปลงนี้สำคัญกับคุณก็ไม่ยากที่จะทำด้วยตนเอง (และถ้ามันไม่สำคัญทำไมคุณต้องกังวลเกี่ยวกับเรื่องนี้?)
การแสดงออกกรณี
พิจารณาสิ่งต่อไปนี้:
foo (0:_ ) = "zero"
foo (1:_ ) = "one"
foo (_:xs) = foo xs
foo ( []) = "end"
สมการสามข้อแรกตรวจสอบทั้งหมดว่ารายการไม่ว่างเปล่า (เหนือสิ่งอื่นใด) แต่การตรวจสอบสิ่งเดียวกันสามครั้งนั้นสิ้นเปลือง โชคดีที่มันเป็นเรื่องง่ายมากสำหรับคอมไพเลอร์ที่จะปรับเรื่องนี้ให้เหมาะกับการแสดงออกกรณีเล็ก ๆ น้อย ๆ ในกรณีนี้สิ่งที่ชอบ
foo xs =
case xs of
y:ys ->
case y of
0 -> "zero"
1 -> "one"
_ -> foo ys
[] -> "end"
ค่อนข้างง่าย แต่มีประสิทธิภาพมากกว่า เนื่องจากคอมไพเลอร์สามารถทำการเปลี่ยนแปลงนี้ได้อย่างง่ายดายคุณจึงไม่ต้องกังวลกับมัน เพียงเขียนการจับคู่รูปแบบของคุณในวิธีที่ง่ายที่สุดเท่าที่จะทำได้ คอมไพเลอร์ดีมากในการจัดลำดับใหม่และจัดเรียงใหม่เพื่อให้เร็วที่สุด
การผสม
สำนวนมาตรฐาน Haskell สำหรับการประมวลผลรายการคือการรวมฟังก์ชั่นเข้าด้วยกันที่รับหนึ่งรายการและสร้างรายการใหม่ ตัวอย่างที่เป็นที่ยอมรับ
map g . map f
น่าเสียดายที่ในขณะที่ความเกียจคร้านรับประกันว่าจะข้ามงานที่ไม่จำเป็น แต่การจัดสรรและยกเลิกการจัดสรรทั้งหมดสำหรับประสิทธิภาพของรายการระดับกลาง "ฟิวชั่น" หรือ "การทำลายป่า" เป็นที่ที่คอมไพเลอร์พยายามกำจัดขั้นตอนกลางเหล่านี้
ปัญหาคือฟังก์ชั่นเหล่านี้ส่วนใหญ่จะเรียกซ้ำ หากไม่มีการเรียกซ้ำมันจะเป็นการฝึกเบื้องต้นในการซับฟังก์ชั่นทั้งหมดลงในบล็อคโค้ดขนาดใหญ่หนึ่งเรียกใช้ซิมป์มันร์และสร้างโค้ดที่ดีที่สุดโดยไม่มีลิสต์กลาง แต่เนื่องจากการเรียกซ้ำจึงไม่ทำงาน
คุณสามารถใช้{-# RULE #-}
pragmas เพื่อแก้ไขปัญหานี้ได้ ตัวอย่างเช่น,
{-# RULES "map/map" forall f g xs. map f (map g xs) = map (f.g) xs #-}
ตอนนี้ทุกครั้งที่ GHC เห็นว่ามีmap
การนำไปใช้map
งานมันจะทำให้มันกลายเป็น pass pass อันเดียวในรายการ
ปัญหาคืองานนี้เฉพาะตามมาด้วยmap
map
มีความเป็นไปอื่น ๆ อีกมากมาย - map
ตามมาด้วยfilter
, filter
ตามมาด้วยmap
ฯลฯ แทนที่จะมือรหัสทางออกสำหรับแต่ละของพวกเขาเรียกว่า "สตรีมฟิวชั่น" ถูกคิดค้น นี่เป็นกลอุบายที่ซับซ้อนมากขึ้นซึ่งฉันจะไม่อธิบายที่นี่
ยาวและระยะสั้นของมันคือเหล่านี้ล้วนเป็นเทคนิคการเพิ่มประสิทธิภาพพิเศษที่เขียนโดยโปรแกรมเมอร์ GHC เองไม่รู้อะไรเลยเกี่ยวกับการหลอมรวม; มันคือทั้งหมดในไลบรารีรายการและไลบรารีคอนเทนเนอร์อื่น ๆ ดังนั้นสิ่งที่ออปติไมซ์เกิดขึ้นนั้นขึ้นอยู่กับวิธีเขียนไลบรารีคอนเทนเนอร์ของคุณ (หรือสมจริงยิ่งขึ้นซึ่งเป็นไลบรารี่ที่คุณเลือกใช้)
ตัวอย่างเช่นหากคุณทำงานกับอาร์เรย์ Haskell '98 อย่าคาดหวังว่าจะมีการรวมฟิวชั่นใด ๆ แต่ฉันเข้าใจว่าvector
ห้องสมุดมีความสามารถในการหลอมรวมอย่างกว้างขวาง มันเกี่ยวกับห้องสมุด คอมไพเลอร์ให้RULES
pragma (ซึ่งมีประสิทธิภาพมากโดยวิธีการในฐานะผู้เขียนห้องสมุดคุณสามารถใช้มันเพื่อเขียนรหัสลูกค้า!)
Meta:
ยอดคงเหลือในทุกสิ่งและทุกสิ่งที่ ...