ใครสามารถอธิบายแนวคิดเบื้องหลังการบันทึกความจำของ Haskell ได้หรือไม่?


12

(หมายเหตุฉันใส่คำถามที่นี่เพราะมันเกี่ยวกับกลไกเชิงแนวคิดของมันไม่ใช่ปัญหาการเข้ารหัส)

ผมทำงานในโปรแกรมขนาดเล็กที่ใช้ลำดับของตัวเลข fibonacci ใน equasion ของมัน แต่ผมสังเกตเห็นว่าถ้าผมได้มากกว่าจำนวนหนึ่งมันก็เจ็บปวดช้า googling รอบ bit ฉัน stumbled เมื่อเทคนิคใน Haskell ที่รู้จักกันเป็นMemoization, พวกเขาแสดงรหัสทำงานเช่นนี้:

-- Traditional implementation of fibonacci, hangs after about 30
slow_fib :: Int -> Integer
slow_fib 0 = 0
slow_fib 1 = 1
slow_fib n = slow_fib (n-2) + slow_fib (n-1)

-- Memorized variant is near instant even after 10000
memoized_fib :: Int -> Integer
memoized_fib = (map fib [0 ..] !!)
   where fib 0 = 0
         fib 1 = 1
         fib n = memoized_fib (n-2) + memoized_fib (n-1)

ดังนั้นคำถามของฉันกับพวกคุณคืออะไรทำไมถึงใช้งานได้ดี

เป็นเพราะมันจะจัดการให้ทำงานผ่านรายการส่วนใหญ่ก่อนที่การคำนวณจะเกิดขึ้นหรือไม่ แต่ถ้าฮาเซลมีสันหลังยาวก็ไม่มีการคำนวณใด ๆ ที่จำเป็นต้องติดตาม ... ดังนั้นมันจะทำงานอย่างไร


1
คุณช่วยอธิบายความหมายของthe calculation catches upอะไรได้บ้าง. BTW การบันทึกช่วยจำไม่ได้เฉพาะสำหรับ haskell: en.wikipedia.org/wiki/Memoization
Simon Bergot

ดูคำอธิบายของฉันภายใต้คำตอบของ killan
กาแฟไฟฟ้า

2
รักคำถามของคุณ เพียงบันทึกย่อ: เทคนิคเรียกว่า memo i zation ไม่ใช่ memo ri zation
Racheet

คำตอบ:


11

เพียงเพื่ออธิบายกลไกเบื้องหลังการบันทึกจริง

memo_fib = (map fib [1..] !!)

สร้างรายการของ "thunks" การคำนวณที่ไม่ได้ประเมินค่า ลองคิดถึงของขวัญที่ยังไม่เปิดตราบใดที่เราไม่แตะมันพวกเขาจะไม่วิ่ง

ตอนนี้เมื่อเราประเมินอันธพาลเราก็ไม่เคยประเมินมันอีกเลย นี่คือรูปแบบเดียวของการกลายพันธุ์ใน "ปกติ" ฮาเซล, thunks กลายพันธุ์เมื่อประเมินว่าเป็นค่าที่เป็นรูปธรรม

ดังนั้นกลับไปที่รหัสของคุณคุณมีรายการ thunks และคุณยังคงเรียกใช้ทรีนี้ แต่คุณ recurse ใช้รายการและเมื่อองค์ประกอบในรายการถูกประเมินมันจะไม่ถูกคำนวณอีกเลย ดังนั้นเราจึงหลีกเลี่ยงการเรียกซ้ำต้นไม้ในฟังก์ชั่นตอแหลๆ

ในฐานะที่เป็นบันทึกย่อที่น่าสนใจโดยเฉพาะนี่คือการคำนวณที่รวดเร็วกว่าชุดของตัวเลข fibonnaci เนื่องจากรายการนั้นได้รับการประเมินเพียงครั้งเดียวหมายความว่าหากคุณคำนวณmemo_fib 10000สองครั้งครั้งที่สองควรเป็นทันที นี่เป็นเพราะ Haskell ประเมินค่าอาร์กิวเมนต์เพียงครั้งเดียวและคุณใช้แอปพลิเคชั่นบางส่วนแทนแลมบ์ดา

TLDR: โดยการจัดเก็บการคำนวณในรายการแต่ละองค์ประกอบของรายการจะถูกประเมินหนึ่งครั้งดังนั้นแต่ละหมายเลข fibonnacci จะถูกคำนวณเพียงครั้งเดียวตลอดทั้งโปรแกรม

การแสดง:

 [THUNK_1, THUNK_2, THUNK_3, THUNK_4, THUNK_5]
 -- Evaluating THUNK_5
 [THUNK_1, THUNK_2, THUNK_3, THUNK_4, THUNK_3 + THUNK_4]
 [THUNK_1, THUNK_2, THUNK_1 + THUNK_2, THUNK_4, THUNK_3 + THUNK_4]
 [1, 1, 1 + 1, THUNK_4, THUNK_3 + THUNK_4]
 [1, 1, 2, THUNK_4, 2 + THUNK4]
 [1, 1, 2, 1 + 2, 2 + THUNK_4]
 [1, 1, 2, 3, 2 + 3]
 [1, 1, 2, 3, 5]

ดังนั้นคุณสามารถดูวิธีการประเมินTHUNK_4ได้เร็วขึ้นมากเนื่องจาก subexpressions นั้นได้รับการประเมินแล้ว


คุณสามารถให้ตัวอย่างของค่าในรายการสำหรับลำดับสั้น ๆ ได้อย่างไร ฉันคิดว่ามันอาจเพิ่มการสร้างภาพข้อมูลว่ามันควรจะทำงานอย่างไร ... และในขณะที่มันเป็นความจริงที่ว่าถ้าฉันโทรmemo_fibด้วยค่าเดียวกันสองครั้งครั้งที่สองจะเป็นทันที แต่ถ้าฉันเรียกมันว่ามีค่าสูงกว่า 1 ยังคงใช้เวลาตลอดไปในการประเมิน (เช่นพูดจาก 30 ถึง 31)
กาแฟไฟฟ้า

@ElectricCoffee เพิ่ม
Daniel Gratzer

@ElectricCoffee ไม่มันจะไม่นับmemo_fib 29และmemo_fib 30ได้รับการประเมินแล้วมันจะใช้เวลานานเท่าที่จะเพิ่มตัวเลขสองตัวนั้น :) เมื่อสิ่งที่ eval-ed ยังคงอยู่
Daniel Gratzer

1
@ElectricCoffee การเรียกซ้ำของคุณต้องผ่านรายการมิฉะนั้นคุณจะไม่ได้รับการแสดงใด ๆ เลย
Daniel Gratzer

2
@ElectricCoffee ใช่ แต่องค์ประกอบที่ 31 ของรายการไม่ได้ใช้การคำนวณที่ผ่านมาคุณกำลังบันทึกใช่ แต่ด้วยวิธีที่ไร้ประโยชน์ .. การคำนวณที่เกิดขึ้นซ้ำไม่ได้คำนวณสองครั้ง แต่คุณยังมีการเรียกใช้ทรีใหม่สำหรับแต่ละค่าใหม่ซึ่งเป็น ช้ามาก ๆ
Daniel Gratzer

1

จุดบันทึกช่วยจำไม่เคยคำนวณฟังก์ชั่นเดียวกันสองครั้ง - สิ่งนี้มีประโยชน์อย่างมากในการเพิ่มความเร็วในการคำนวณที่ใช้งานได้อย่างหมดจดนั่นคือไม่มีผลข้างเคียงเพราะสำหรับกระบวนการเหล่านั้นสามารถเป็นไปโดยอัตโนมัติทั้งหมด สิ่งนี้มีความจำเป็นอย่างยิ่งสำหรับฟังก์ชั่นเช่นfiboซึ่งนำไปสู่การเรียกใช้ต้นไม้ซ้ำเช่นความพยายามอธิบายเมื่อนำมาใช้อย่างไร้เดียงสา (นี่คือเหตุผลหนึ่งว่าทำไมตัวเลข Fibonacci จึงเป็นตัวอย่างที่ไม่ดีอย่างมากสำหรับการสอนการเรียกซ้ำ - การสาธิตการใช้งานเกือบทั้งหมดที่คุณพบในบทช่วยสอนหรือหนังสือไม่สามารถใช้ได้สำหรับค่าอินพุตจำนวนมาก)

หากคุณติดตามการไหลของการดำเนินการคุณจะเห็นว่าในกรณีที่สองค่าสำหรับfib xจะพร้อมใช้งานเสมอเมื่อfib x+1มีการดำเนินการและระบบรันไทม์จะสามารถอ่านได้จากหน่วยความจำแทนการเรียกซ้ำแบบอื่นในขณะที่ วิธีแก้ไขปัญหาแรกพยายามคำนวณโซลูชันที่มีขนาดใหญ่กว่าก่อนที่ผลลัพธ์สำหรับค่าที่น้อยกว่าจะพร้อมใช้งาน นี่คือท้ายที่สุดเนื่องจากตัววนซ้ำ[0..n]ถูกประเมินจากซ้ายไปขวาและจะเริ่มต้นด้วย0ในขณะที่การเรียกซ้ำในตัวอย่างแรกเริ่มต้นด้วยnแล้วถามn-1เท่านั้น นี่คือสิ่งที่นำไปสู่การเรียกใช้ฟังก์ชันที่ซ้ำซ้อนจำนวนมากที่ไม่จำเป็นจำนวนมาก


โอ้ฉันเข้าใจจุดนั้นฉันแค่ไม่เข้าใจว่ามันทำงานอย่างไรเช่นจากสิ่งที่ฉันเห็นในโค้ดคือเมื่อคุณเขียนmemorized_fib 20ตัวอย่างคุณจริง ๆ แล้วคุณเพิ่งเขียนmap fib [0..] !! 20มันจะต้องคำนวณ ช่วงทั้งหมดของจำนวนมากถึง 20 หรือว่าฉันขาดอะไรบางอย่างที่นี่?
กาแฟไฟฟ้า

1
ใช่ แต่เพียงครั้งเดียวสำหรับแต่ละหมายเลข คำนวณการดำเนินงานที่ไร้เดียงสาfib 2จึงมักจะทำให้สปินหัวของคุณ - n==5ไปข้างหน้าเขียนลงขนต้นไม้โทรเพียงค่าขนาดเล็กเช่น คุณจะไม่มีวันลืมการบันทึกอีกครั้งเมื่อคุณเห็นสิ่งที่มันช่วยคุณ
Kilian Foth

@ElectricCoffee: ใช่มันจะคำนวณค่า fib ของ 1 ถึง 20 คุณไม่ได้รับอะไรเลยจากการโทรนั้น ตอนนี้ลองคำนวณ fib 21 และคุณจะเห็นว่าแทนที่จะคำนวณ 1-21 คุณสามารถคำนวณ 21 ได้เพราะคุณมีการคำนวณ 1-20 แล้วและไม่จำเป็นต้องทำอีก
Phoshi

ฉันพยายามที่จะเขียนทรีสายสำหรับn = 5และในขณะนี้ฉันมาถึงจุดที่n == 3จนถึงตอนนี้ดีมาก แต่บางทีมันอาจเป็นเพียงความคิดที่จำเป็นของฉันกำลังคิดเรื่องนี้ แต่นั่นก็ไม่ได้หมายความว่าn == 3คุณจะได้map fib [0..]!!3? ซึ่งจะเข้าสู่fib nสาขาของโปรแกรม ... ฉันจะได้รับประโยชน์จากข้อมูลที่คำนวณล่วงหน้าได้ที่ไหน
กาแฟไฟฟ้า

1
ไม่memoized_fibเป็นไร มันslow_fibจะทำให้คุณร้องไห้ถ้าคุณตามรอย
Kilian Foth
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.