มีอยู่วันหนึ่งอาจารย์ผมให้ลองเขียน fibonacci แบบ recursive ใน c++ ซึ่งถ้าทำ fibo ซัก 40 ตัวก็เริ่มอืดๆ แล้วถ้า 50 ตัว ไม่ต้องพูดถึง พอใช้เขียนแบบ dynamic programming มันก็เร็วขึ้นแบบทันตาด้วยความตื่นเต้นกับ dynamic programming เลยไปเล่าให้เพื่อนฟัง
ทีนี้ปัญหาที่ผมสงสัยก็คือพอฟังเพื่อนก็ลองเขียนลงไปใน ruby มันก็ช้าเหมือน c++ แต่พอเขียนด้วย haskell ปรากฎว่ามันเร็วแบบทันตาเห็นเหมือนกับที่เขียนแบบ dynamic programming เลยครับ อยากจะทราบว่าทำไมมันเป็นแบบนั้นครับ
ขอบคุณครับ
กระทู้เก่าๆ จะย้ายตามไปในภายหลัง ตอนนี้ปิดการโพสต์กระทู้ไว้ เหลือไว้เฉพาะอ้างอิงเท่านั้น
เดาว่า มันไม่มี stack
tail recursion optimization ทำได้ก็ต่อเมื่อ function นั้นต้อง return ค่าที่ได้จากการเรียกตัวเอง ไปโดยไม่ไปคำนวนอะไรซ้ำ หรือเปล่า? (จำเป็นเปล่า?)
แบบประมาณว่าจริงใช้ค่าที่ return ครั้งสุดท้ายก็พอ ไม่ต้องเก็บว่าก่อนหน้านั้น return อะไรมา? เช่น tata(5) -> tata(4) -> tata(3) -> tata(2) -> tata(1) จริงๆ ถ้าหา tata(1) ได้ก็เสร็จเลย เอาค่านั้นมาตอบได้เลย. แต่พอใช้ท่านี้แล้วมักจะต้องเพิ่ม accumulator เข้ามา. แต่ไม่รู้ Haskell ทำไง :-P.
ไม่เห็นเอา code มาให้ดูเลย. ผมก็ไม่รู้หรอกครับ แต่อยากดู code ด้วย. :-D
นี่โค้ด ruby นะครับ
def fib(n)
if n<2 then
1
else
fib(n-1)+fib(n-2)
end
end
อยากได้ code haskell อะครับ เวลาพิมพ์ code ใช้ tag ชื่อ blockcode แล้วจะออกมาสวยครับ. :-)
ส่วนนี่เป็น haskell ครับ
fibo 0 = 0
fibo 1 = 1
fibo n = fibo (n-2) + fibo (n-1)
เห็นงี้ผมก็จอดแล้วครับ
ถ้าเป็นแบบข้างล่างยังพออธิบายได้บ้างว่าเป็น tail recursion
function fib(n, acc) { if(n<1) return 1; acc = acc * n; return fib(n - 1, acc); }เล่น haskell เป็นลูกศิษอ.เดฟปะครับ? (ตอบคำถามไม่ได้ ก็พาออกนอกเรื่องไป :-P)
ใช่ครับ (แม่นแฮะ ^^)
ถ้าเกิดว่ารู้เฉลยจากที่อื่น ไม่ว่าจะมีคนอื่นบอก หรืออ่านวิกีพีเดีย หรือ search google มา อย่าลืมมาเล่าสู่กันฟังบ้างนะครับ ผมก็อยากรู้เหมือนกัน.
ครับ ^^
เคยอ่าน haskell เมื่อนานมากแล้ว แล้วก็ไม่ได้ใช้ เ่ท่าที่จำได้มันไม่ได้ทำงานเหมือน function แต่เป็นการแปลงขณะ interpret เหลือแค่บวกกันเฉยๆ
อันนี้ไม่แน่ใจนะครับ ลองเอาคำตอบนี้ไป search ยืนยันอีกที
http://en.wikibooks.org/wiki/Haskell/Haskell_Performance
ยืนยันความคิด ข้างบน คือใช้ lazy evaluation แทนการเจอ function เมื่อไหนก็เรียก function
ทำให้ ไม่ต้องใช้ stack ในกรณีที่ reduce form ได้
ดีกว่าอ่านในหน้า Haskell ของวิกิพีเดียแฮะ.
ตอนแรกผมก็คิดทำนองนี้นะ
ไหนๆ ก็เป็น functional ที่ no side effect แล้ว
ถ้าเรียกซ้ำด้วย argument เดิม ก็ควรจะไม่ต้องไปคำนวณซ้ำซ้อนอีก
แต่ผลการทดลอง ที่ทดลองหา fib(50) ที่หายต๋อมไป (> 3 นาที ขี้เกียจรอ เลยไม่รู้ว่าต้องใช้เวลาเท่าไร)
มันเหมือนกับว่า มันไม่ได้เกิด reduce form เลยนี่ครับ
Note: คำตอบนี้เดาเอาเหมือนกัน, และหวังว่าคำถามนี้จะไม่ใช่การบ้านอาจารย์เดฟด้วยนะ
ลอง code นี้ดูนะ
ลอง run แบบ interpreter ด้วย hugs
ลอง run แบบ interpreter ด้วย ghc
จะเห็นว่า แต่ละ implement ก็มีความเก่งไม่เท่ากัน
ที่นี้ลองเทียบกับ ruby ดูบ้าง
code แบบนี้
จับเวลาดู
กลายเป็นว่า interpreter mode, ruby ทำงานเร็วกว่า
ทีนี้ลอง compile เป็น native ดูบ้าง
เร็วขึ้นแบบสุดๆ
ถามว่ามันเร็วได้อย่างไร
ถ้าดูอย่างนี้ ก็ต้องเดาตอบว่าเพราะ optimizer ของ ghc
เนื่องจาก haskell มันเป็น pure functional language
ไม่มี side effect
การที่เราเรียก fib(ุx) ไม่ว่าจะกี่ครั้งก็ตาม เนื่องจากไม่มี side effect
มันก็ต้องได้คำตอบเดิมออกมาเสมอ
ดังนั้นคาดว่า ghc คงใส่กลไก memoization เข้าไปให้ด้วย
(ซึ่งมันก็คือ dynamic programming แบบ top-down approch นั่นเอง)
แก้ไขข้างบน
> ดังนั้นคาดว่า ghc คงใส่กลไก memoization เข้าไปให้ด้วย
> (ซึ่งมันก็คือ dynamic programming แบบ top-down approch นั่นเอง)
เดาผิด, มันไม่ได้ทำอย่างนั้นหรอก
เพราะลองเปลี่ยนเป็น fib(50) แล้ว ก็หายต๋อมไปเลย
สรุปว่า ไม่รู้ ghc มัน implement อย่างไรเหมือนกัน
หลังจากทบทวนดูอีกที
เราหลงทางกับคำถามเองนี่หว่า
สรุปว่า haskell เร็วเพราะ มัน compile เป็น native (no more no less)
แต่ถ้า run ใน interpreter mode แล้ว ช้ากว่า ruby เสียอีก
ไม่ใช่การบ้าน อ.เดฟ หรอกครับผมสงสัยเฉยๆ เลยมาโพสต์ถาม
้ที่บอกว่าเร็วพอๆกับ version dynamic นั้น
เครื่องผมไม่ได้ผลแบบนั้นนะ
bentino ลองใช้ C++ แล้วทำการทดลองแบบ pphetra ได้เปล่า? ลองเทียบ C++ recursive กับอย่างอื่นๆ.
ขออ่านหนังสือสอบก่อนนะครับ (อาทิตย์นี้แล้ว) เดี๋ยวได้ผลอย่างไรจะมาโพสต์ให้ดูครับ
ครับ รอดูนะ. ว่าจะลองเขียน ocaml หรือ scheme มาประกวดบ้างดีมะ :-P.
อันนี้แบบ python
บนเครื่องผม ถ้าเป็น recursive c
เทียบกับ ghc ข้างบน ที่ใช้เวลา 0.5xx แล้ว magnitude ก็ระดับ 10x
ผมคิดว่าการคำนวน fibonaci จะช้าหรือเร็ว ในกรณีนี้ไม่น่าจะขึ้นอยู่กับภาษาเขียนโปรแกรม แต่น่าจะอยู่ที่วิธีการมากกว่าครับ ผมได้ลองนับจำนวนการเรียกฟังก์ชั่นดู ปรากฏว่าได้ผลดังนี้ครับ
ผลการรันโปรแกรม
จากกผลอันนี้ สรุปได้ว่า จำนวนครั้งในการเรียกฟังก์ชั่น เท่ากับ 2*fibonaci -1 ซึ่งแน่นอนว่า ไม่ว่าภาษาไหนก็ต้องอืดเป็นธรรมดา แต่ haskell จะเร็วกว่าภาษาอื่น เพราะว่า ไม่มี if (เดาเอา) หาก n = 30 และใช้ haskell ก็จะประหยัดการใช้ if ไปได้ 1664079 ! ครั้ง ผมคิดว่ามีผลกับ performance แน่นอน
วิธีแก้ที่ถูกจึงไม่ใช่เปลี่ยนภาษาเขียนโปรแกรมครับ แต่น่าจะเปลี่ยน algorithm มากกว่า ยกตัวอย่างเช่น
ฟังก็ชั่นนี้มีการวนลูป เท่ากับ n-2 ทำให้ประหยัดการทำงานของโปรแกรมได้เยอะอย่างประมาณไม่ได้ ยกตัวอย่างเช่น ผมหาค่า fibo(100000) ซึ่งมีค่าประมาณ 2.597e20899 (สิบยกกำลังสองหมื่นกว่า ๆ) ได้ในเวลาไม่ถึงหนึ่งวินาที เพราะมีการวนลูปเพียง 999998 ครั้ง แต่หากใช้ recursive ต้องเรียกฟังก์ชั่นกัน 5.194e20899 ครั้ง
ใช้ท่า recursive แบบโกงหน่อยได้ก็เร็วขึ้น
แต่ก็จะไปติดปัญหาว่า stack เต็มแทน
เปลี่ยนมาใช้ scheme ที่มี tail recursive แทนก็ทำได้
ใช้ mzscheme เกือบๆ 4 วินาทีก็ออก
real 0m3.396s
user 0m2.347s
sys 0m0.440s
แต่หลายคนก็อาจจะบอกว่าจะเขียน recursion ทำไม :-P ... สำหรับแล้วตอบว่า "ขำๆ".
เขียนง่าย/ตรง definition มั้ง
ขำๆ
พอมี accumlator แล้วก็ห่าง definition ออกไปอีกอะครับ. แต่ดู condition ข้างในก็ยังเหลือเค้า.