การบันทึกอัตโนมัติใน GHC Haskell เป็นอย่างไร


106

ฉันคิดไม่ออกว่าทำไม m1 จึงถูกบันทึกในขณะที่ m2 ไม่ได้อยู่ในสิ่งต่อไปนี้:

m1      = ((filter odd [1..]) !!)

m2 n    = ((filter odd [1..]) !! n)

m1 10,000000 ใช้เวลาประมาณ 1.5 วินาทีในการโทรครั้งแรกและเศษของการโทรครั้งต่อ ๆ ไป (น่าจะเป็นการแคชรายการ) ในขณะที่ m2 10,000000 จะใช้เวลาเท่ากันเสมอ (การสร้างรายการใหม่ด้วยการโทรแต่ละครั้ง) มีความคิดเกิดอะไรขึ้น? มีกฎง่ายๆว่า GHC จะบันทึกฟังก์ชันหรือไม่และเมื่อใด ขอบคุณ.

คำตอบ:


112

GHC ไม่บันทึกฟังก์ชัน

อย่างไรก็ตามจะคำนวณนิพจน์ใด ๆ ที่กำหนดในโค้ดได้มากที่สุดครั้งละครั้งต่อครั้งที่มีการป้อน lambda-expression โดยรอบหรือมากที่สุดครั้งเดียวหากอยู่ในระดับบนสุด การพิจารณาว่านิพจน์แลมบ์ดาอยู่ที่ใดอาจเป็นเรื่องยุ่งยากเล็กน้อยเมื่อคุณใช้น้ำตาลวากยสัมพันธ์เช่นในตัวอย่างของคุณดังนั้นเรามาแปลงสิ่งเหล่านี้เป็นไวยากรณ์ที่ได้รับการ desugared ที่เทียบเท่ากัน:

m1' = (!!) (filter odd [1..])              -- NB: See below!
m2' = \n -> (!!) (filter odd [1..]) n

(หมายเหตุ: จริงๆแล้วรายงาน Haskell 98 อธิบายถึงส่วนของตัวดำเนินการด้านซ้ายเช่นเดียว(a %)กับที่เทียบเท่า\b -> (%) a bแต่ GHC จะแยก(%) aออกเป็นส่วนนี้มีความแตกต่างกันในทางเทคนิคเนื่องจากสามารถแยกแยะseqได้ฉันคิดว่าฉันอาจส่งตั๋ว GHC Trac เกี่ยวกับเรื่องนี้)

ให้นี้คุณสามารถเห็นในที่m1'แสดงออกfilter odd [1..]ไม่อยู่ในที่ใด ๆ แลมบ์ดาแสดงออกจึงจะได้รับการคำนวณครั้งเดียวต่อการทำงานของโปรแกรมของคุณในขณะที่ในm2', filter odd [1..]จะได้รับการคำนวณในแต่ละครั้งที่แลมบ์ดาแสดงออกถูกป้อนคือ m2'ในสายของแต่ละ ซึ่งอธิบายถึงความแตกต่างของเวลาที่คุณเห็น


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

f = \x -> let y = [1..30000000] in foldl' (+) 0 (y ++ [x])

GHC อาจสังเกตว่าyไม่ขึ้นอยู่กับxและเขียนฟังก์ชันใหม่เป็น

f = let y = [1..30000000] in \x -> foldl' (+) 0 (y ++ [x])

ในกรณีนี้เวอร์ชันใหม่มีประสิทธิภาพน้อยกว่ามากเนื่องจากจะต้องอ่านข้อมูลประมาณ 1 GB จากหน่วยความจำที่yจัดเก็บในขณะที่เวอร์ชันดั้งเดิมจะทำงานในพื้นที่คงที่และพอดีกับแคชของโปรเซสเซอร์ ในความเป็นจริงภายใต้ GHC 6.12.1, ฟังก์ชั่นfเป็นเกือบสองเท่าอย่างรวดเร็วเมื่อรวบรวมได้โดยไม่ต้อง-O2เพิ่มประสิทธิภาพกว่าที่มันจะรวบรวมกับ


1
ค่าใช้จ่ายในการประเมินนิพจน์ (กรองคี่ [1 .. ]) ใกล้เคียงกับศูนย์อยู่แล้ว - มันเป็นรายการที่ขี้เกียจดังนั้นต้นทุนจริงจึงอยู่ในแอปพลิเคชัน (x !! 10,000000) เมื่อมีการประเมินรายการจริง นอกจากนี้ทั้ง m1 และ m2 ดูเหมือนว่าจะได้รับการประเมินเพียงครั้งเดียวด้วย -O2 และ -O1 (บน ghc 6.12.3 ของฉัน) อย่างน้อยในการทดสอบต่อไปนี้: (test = m1 10,000000 seqm1 10000000) มีความแตกต่างแม้ว่าเมื่อไม่มีการระบุแฟล็กการปรับให้เหมาะสม และตัวแปร "f" ทั้งสองของคุณมีที่อยู่อาศัยสูงสุด 5356 ไบต์โดยไม่คำนึงถึงการเพิ่มประสิทธิภาพอย่างไรก็ตาม (มีการจัดสรรรวมน้อยลงเมื่อใช้ -O2)
Ed'ka

1
@ Ed'ka: ลองใช้โปรแกรมทดสอบนี้ด้วยคำจำกัดความข้างต้นของf: main = interact $ unlines . (show . map f . read) . lines; รวบรวมโดยมีหรือไม่มี-O2; แล้วecho 1 | ./main. หากคุณเขียนแบบทดสอบmain = print (f 5)คุณyสามารถเก็บขยะได้ตามที่ใช้งานและไม่มีความแตกต่างระหว่างสองfs
Reid Barton

เอ้อที่ควรจะเป็นmap (show . f . read)ของหลักสูตร และตอนนี้ฉันดาวน์โหลด GHC 6.12.3 แล้วฉันเห็นผลลัพธ์เช่นเดียวกับใน GHC 6.12.1 และใช่คุณมีสิทธิเกี่ยวกับการเดิมm1และm2: รุ่น GHC ที่ดำเนินการชนิดนี้ในการยกด้วยการเพิ่มประสิทธิภาพการเปิดใช้งานจะเปลี่ยนเข้าสู่m2 m1
Reid Barton

ใช่ตอนนี้ฉันเห็นความแตกต่างแล้ว (-O2 ช้ากว่าแน่นอน) ขอบคุณสำหรับตัวอย่างนี้!
Ed'ka

29

m1 คำนวณเพียงครั้งเดียวเนื่องจากเป็นแบบฟอร์มการใช้งานคงที่ในขณะที่ m2 ไม่ใช่ CAF และจะคำนวณสำหรับการประเมินแต่ละครั้ง

ดู GHC wiki เกี่ยวกับ CAF: http://www.haskell.org/haskellwiki/Constant_applicative_form


1
คำอธิบาย "m1 คำนวณเพียงครั้งเดียวเนื่องจากเป็นแบบฟอร์มการสมัครคงที่" ไม่สมเหตุสมผลสำหรับฉัน เนื่องจากสมมติว่าทั้ง m1 และ m2 เป็นตัวแปรระดับบนสุดฉันคิดว่าฟังก์ชันเหล่านี้คำนวณเพียงครั้งเดียวไม่ว่าจะเป็น CAF หรือไม่ก็ตาม ความแตกต่างคือรายการ[1 ..]จะคำนวณเพียงครั้งเดียวในระหว่างการทำงานของโปรแกรมหรือคำนวณครั้งเดียวต่อแอปพลิเคชันของฟังก์ชัน แต่เกี่ยวข้องกับ CAF หรือไม่
Tsuyoshi Ito

1
จากหน้าที่เชื่อมโยง: "A CAF ... สามารถรวบรวมเป็นส่วนหนึ่งของกราฟซึ่งจะแชร์โดยการใช้งานทั้งหมดหรือไปยังโค้ดที่ใช้ร่วมกันบางส่วนซึ่งจะเขียนทับตัวเองด้วยกราฟบางส่วนในครั้งแรกที่มีการประเมิน" เนื่องจากm1เป็น CAF อย่างที่สองใช้และfilter odd [1..](ไม่ใช่แค่[1..]!) GHC ยังสามารถสังเกตได้ว่าm2อ้างถึงfilter odd [1..]และวางลิงก์ไปยัง thunk เดียวกันกับที่ใช้m1แต่นั่นอาจเป็นความคิดที่ไม่ดี: อาจทำให้หน่วยความจำรั่วไหลในบางสถานการณ์
Alexey Romanov

@ Alexey: ขอบคุณสำหรับการแก้ไข[1..]และfilter odd [1..]. ส่วนที่เหลือฉันยังไม่มั่นใจ ถ้าฉันจำไม่ผิด CAF จะเกี่ยวข้องก็ต่อเมื่อเราต้องการโต้แย้งว่าคอมไพเลอร์สามารถแทนที่filter odd [1..]อินm2ด้วย global thunk ได้ (ซึ่งอาจจะเหมือนกันกับที่ใช้ในm1) แต่ในสถานการณ์ของผู้ถามคอมไพลเลอร์ไม่ได้ทำ "การเพิ่มประสิทธิภาพ" นั้นและฉันไม่เห็นความเกี่ยวข้องกับคำถาม
Tsuyoshi Ito

2
มันมีความเกี่ยวข้องที่จะสามารถแทนที่ใน m1และมันไม่
Alexey Romanov

13

มีความแตกต่างที่สำคัญระหว่างสองรูปแบบ: ข้อ จำกัด monomorphism ใช้กับ m1 แต่ไม่ใช่ m2 เนื่องจาก m2 ได้ระบุอาร์กิวเมนต์ไว้อย่างชัดเจน ประเภทของ m2 จึงเป็นแบบทั่วไป แต่ m1 มีความเฉพาะเจาะจง ประเภทที่กำหนด ได้แก่ :

m1 :: Int -> Integer
m2 :: (Integral a) => Int -> a

คอมไพเลอร์และล่าม Haskell ส่วนใหญ่ (ทั้งหมดที่ฉันรู้จัก) ไม่จดจำโครงสร้างโพลีมอร์ฟิกดังนั้นรายการภายในของ m2 จึงถูกสร้างขึ้นใหม่ทุกครั้งที่เรียกโดยที่ m1 ไม่ใช่


1
การเล่นกับสิ่งเหล่านี้ใน GHCi ดูเหมือนว่ามันขึ้นอยู่กับการแปลงแบบปล่อยให้ลอย (หนึ่งในการเพิ่มประสิทธิภาพของ GHC ที่ไม่ได้ใช้ใน GHCi) และแน่นอนว่าเมื่อรวบรวมฟังก์ชันง่ายๆเหล่านี้เครื่องมือเพิ่มประสิทธิภาพจะสามารถทำให้ฟังก์ชันเหล่านี้ทำงานเหมือนเดิมได้อยู่ดี (ตามการทดสอบเกณฑ์บางอย่างที่ฉันใช้อยู่โดยมีฟังก์ชันในโมดูลแยกต่างหากและทำเครื่องหมายด้วยบรรทัดฐาน NOINLINE) น่าจะเป็นเพราะการสร้างรายการและการจัดทำดัชนีจะหลอมรวมเป็นลูปที่แน่นมาก
mokus

1

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

เห็นได้ชัดว่ามีกลไกการปรับให้เหมาะสมในคอมไพเลอร์ GHC ที่ใช้ประโยชน์จากข้อเท็จจริงนี้เพื่อคำนวณค่าของฟังก์ชันดังกล่าวเพียงครั้งเดียวสำหรับรันไทม์ของโปรแกรมทั้งหมด มันทำอย่างเฉื่อยชาเพื่อให้แน่ใจ แต่กระนั้นมันก็เป็นเช่นนั้น ฉันสังเกตตัวเองเมื่อฉันเขียนฟังก์ชันต่อไปนี้:

primes = filter isPrime [2..]
    where isPrime n = null [factor | factor <- [2..n-1], factor `divides` n]
        where f `divides` n = (n `mod` f) == 0

จากนั้นก็ให้ทดสอบนั้นผมเข้า GHCI primes !! 1000และเขียน: ใช้เวลาไม่กี่วินาที แต่ในที่สุดฉันก็ได้คำตอบ: 7927. จากนั้นฉันก็โทรไปprimes !! 1001และได้รับคำตอบทันที ในทำนองเดียวกันในทันทีที่ฉันได้ผลลัพธ์take 1000 primesเนื่องจาก Haskell ต้องคำนวณรายการพันองค์ประกอบทั้งหมดเพื่อส่งคืนองค์ประกอบ 1001st ก่อนหน้านี้

ดังนั้นหากคุณสามารถเขียนฟังก์ชันของคุณโดยที่ไม่ต้องใช้พารามิเตอร์คุณก็อาจต้องการมัน ;)

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