Haskell ใช้การประเมินแบบขี้เกียจเพื่อใช้การเรียกซ้ำดังนั้นให้ถือว่าทุกอย่างเป็นสัญญาว่าจะให้ค่าเมื่อจำเป็น (เรียกว่า thunk) Thunks จะลดลงเท่าที่จำเป็นเท่านั้นเพื่อดำเนินการต่อไป สิ่งนี้คล้ายกับวิธีที่คุณทำให้นิพจน์ง่ายขึ้นในทางคณิตศาสตร์ดังนั้นจึงเป็นประโยชน์ที่จะคิดอย่างนั้น ความจริงที่ว่าโค้ดของคุณไม่ได้ระบุลำดับการประเมินทำให้คอมไพเลอร์สามารถทำการเพิ่มประสิทธิภาพที่ชาญฉลาดได้มากมายกว่าการกำจัด tail-call ที่คุณคุ้นเคย รวบรวม-O2
หากคุณต้องการเพิ่มประสิทธิภาพ!
มาดูกันว่าเราประเมินfacSlow 5
เป็นกรณีศึกษาอย่างไร:
facSlow 5
5 * facSlow 4
5 * (4 * facSlow 3)
5 * (4 * (3 * facSlow 2))
5 * (4 * (3 * (2 * facSlow 1)))
5 * (4 * (3 * (2 * 1)))
5 * (4 * (3 * 2))
5 * (4 * 6)
5 * 24
120
เช่นเดียวกับที่คุณกังวลเรามีการสร้างตัวเลขก่อนที่การคำนวณจะเกิดขึ้น แต่ไม่เหมือนกับที่คุณกังวลไม่มีfacSlow
การเรียกใช้ฟังก์ชันซ้อนกันเพื่อรอการสิ้นสุด - การลดแต่ละครั้งจะถูกนำไปใช้และหายไปโดยทิ้งกรอบสแต็กไว้ ปลุก (นั่นเป็นเพราะ(*)
เข้มงวดและทำให้เกิดการประเมินอาร์กิวเมนต์ที่สอง)
ฟังก์ชันเรียกซ้ำของ Haskell ไม่ได้รับการประเมินในลักษณะที่เรียกซ้ำมาก! สายซ้อนเพียงสายเดียวคือการคูณด้วยตัวมันเอง หาก (*)
ถูกมองว่าเป็นตัวสร้างข้อมูลที่เข้มงวดนี่คือสิ่งที่เรียกว่าการเรียกซ้ำที่มีการป้องกัน (แม้ว่าโดยปกติจะอ้างถึงสิ่งนั้นด้วยตัวสร้างข้อมูลที่ไม่จำกัด ซึ่งสิ่งที่เหลืออยู่คือตัวสร้างข้อมูล - เมื่อถูกบังคับโดยการเข้าถึงเพิ่มเติม)
ตอนนี้เรามาดูหางซ้ำfac 5
:
fac 5
fac' 5 1
fac' 4 {5*1}
fac' 3 {4*{5*1}}
fac' 2 {3*{4*{5*1}}}
fac' 1 {2*{3*{4*{5*1}}}}
{2*{3*{4*{5*1}}}}
(2*{3*{4*{5*1}}})
(2*(3*{4*{5*1}}))
(2*(3*(4*{5*1})))
(2*(3*(4*(5*1))))
(2*(3*(4*5)))
(2*(3*20))
(2*60)
120
คุณจะเห็นได้ว่าการเรียกหางซ้ำด้วยตัวมันเองไม่ได้ช่วยคุณประหยัดเวลาหรือพื้นที่ โดยรวมไม่เพียง แต่ใช้เวลามากกว่าขั้นตอนโดยรวมfacSlow 5
เท่านั้น แต่ยังสร้าง thunk ที่ซ้อนกัน (แสดงที่นี่{...}
) - ต้องการพื้นที่พิเศษสำหรับมัน - ซึ่งอธิบายการคำนวณในอนาคตการคูณที่ซ้อนกันที่จะดำเนินการ
thunk นี้จะหลุดแล้วโดยภายในมันไปที่ด้านล่างสูตรการคำนวณในกอง นอกจากนี้ยังมีอันตรายที่จะทำให้สแตกล้นด้วยการคำนวณที่ยาวมากสำหรับทั้งสองเวอร์ชัน
หากเราต้องการเพิ่มประสิทธิภาพด้วยมือสิ่งที่เราต้องทำคือทำให้เข้มงวด คุณสามารถใช้ตัวดำเนินการแอปพลิเคชันที่เข้มงวด$!
เพื่อกำหนด
facSlim :: (Integral a) => a -> a
facSlim x = facS' x 1 where
facS' 1 y = y
facS' x y = facS' (x-1) $! (x*y)
สิ่งนี้บังคับfacS'
ให้เข้มงวดในการโต้แย้งครั้งที่สอง (มันเข้มงวดอยู่แล้วในข้อโต้แย้งแรกเพราะต้องได้รับการประเมินเพื่อตัดสินใจว่าfacS'
จะใช้คำจำกัดความใด)
บางครั้งความเข้มงวดสามารถช่วยได้อย่างมหาศาลบางครั้งก็เป็นความผิดพลาดครั้งใหญ่เพราะความขี้เกียจมีประสิทธิภาพมากกว่า นี่เป็นความคิดที่ดี:
facSlim 5
facS' 5 1
facS' 4 5
facS' 3 20
facS' 2 60
facS' 1 120
120
ซึ่งเป็นสิ่งที่คุณต้องการบรรลุฉันคิดว่า
สรุป
- หากคุณต้องการเพิ่มประสิทธิภาพโค้ดของคุณขั้นตอนที่หนึ่งคือการคอมไพล์ด้วย
-O2
- การเรียกหางซ้ำจะดีก็ต่อเมื่อไม่มีการสะสมของลำตัวและการเพิ่มความเข้มงวดมักจะช่วยป้องกันได้ตามความเหมาะสม สิ่งนี้จะเกิดขึ้นเมื่อคุณสร้างผลลัพธ์ที่จำเป็นในภายหลังพร้อมกัน
- บางครั้งการเรียกซ้ำหางเป็นแผนการที่ไม่ดีและการเรียกซ้ำที่มีการป้องกันนั้นเหมาะสมกว่ากล่าวคือเมื่อผลลัพธ์ที่คุณสร้างขึ้นจะเป็นที่ต้องการทีละนิด ดูคำถามเกี่ยวกับ
foldr
และfoldl
ตัวอย่างนี้แล้วทดสอบกันเอง
ลองสองข้อนี้:
length $ foldl1 (++) $ replicate 1000
"The size of intermediate expressions is more important than tail recursion."
length $ foldr1 (++) $ replicate 1000
"The number of reductions performed is more important than tail recursion!!!"
foldl1
เป็นหางซ้ำในขณะที่foldr1
ดำเนินการเรียกซ้ำที่มีการป้องกันเพื่อให้รายการแรกถูกนำเสนอทันทีสำหรับการประมวลผล / การเข้าถึงเพิ่มเติม ("วงเล็บ" อันแรกไปทางซ้ายพร้อมกันโดย(...((s+s)+s)+...)+s
บังคับให้รายการอินพุตจนสุดและสร้างการคำนวณครั้งใหญ่ในอนาคตเร็วกว่าที่ต้องการผลลัพธ์ทั้งหมดวงเล็บที่สองจะค่อยๆไปทางขวาs+(s+(...+(s+s)...))
โดยใช้อินพุต ทีละรายการดังนั้นสิ่งทั้งหมดจึงสามารถทำงานในพื้นที่คงที่ด้วยการปรับให้เหมาะสม)
คุณอาจต้องปรับจำนวนศูนย์ขึ้นอยู่กับฮาร์ดแวร์ที่คุณใช้