ฉันสงสัยว่าทำไมการใช้งาน Haskell จึงใช้ GC
ฉันไม่สามารถนึกถึงกรณีที่ GC จำเป็นในภาษาที่บริสุทธิ์ เป็นเพียงการเพิ่มประสิทธิภาพเพื่อลดการคัดลอกหรือจำเป็นจริง ๆ ?
ฉันกำลังมองหาโค้ดตัวอย่างที่อาจรั่วไหลหากไม่มี GC
ฉันสงสัยว่าทำไมการใช้งาน Haskell จึงใช้ GC
ฉันไม่สามารถนึกถึงกรณีที่ GC จำเป็นในภาษาที่บริสุทธิ์ เป็นเพียงการเพิ่มประสิทธิภาพเพื่อลดการคัดลอกหรือจำเป็นจริง ๆ ?
ฉันกำลังมองหาโค้ดตัวอย่างที่อาจรั่วไหลหากไม่มี GC
คำตอบ:
เป็นคนอื่นได้ชี้แล้วออก Haskell ต้องอัตโนมัติ , แบบไดนามิกจัดการหน่วยความจำ: จัดการหน่วยความจำอัตโนมัติเป็นสิ่งจำเป็นเพราะการจัดการหน่วยความจำด้วยตนเองไม่ปลอดภัย การจัดการหน่วยความจำแบบไดนามิกเป็นสิ่งจำเป็นเนื่องจากสำหรับบางโปรแกรมอายุการใช้งานของวัตถุสามารถกำหนดได้ที่รันไทม์
ตัวอย่างเช่นพิจารณาโปรแกรมต่อไปนี้:
main = loop (Just [1..1000]) where
loop :: Maybe [Int] -> IO ()
loop obj = do
print obj
resp <- getLine
if resp == "clear"
then loop Nothing
else loop obj
ในโปรแกรมนี้รายการ[1..1000]
จะต้องถูกเก็บไว้ในหน่วยความจำจนกว่าผู้ใช้จะพิมพ์ "clear"; ดังนั้นจึงต้องกำหนดอายุการใช้งานแบบไดนามิกและนี่คือเหตุผลที่จำเป็นต้องมีการจัดการหน่วยความจำแบบไดนามิก
ดังนั้นในแง่นี้การจัดสรรหน่วยความจำแบบไดนามิกอัตโนมัติจึงจำเป็นและในทางปฏิบัติหมายความว่าใช่ Haskell ต้องการตัวเก็บขยะเนื่องจากการรวบรวมขยะเป็นตัวจัดการหน่วยความจำแบบไดนามิกอัตโนมัติที่มีประสิทธิภาพสูงสุด
แม้ว่าตัวเก็บขยะจะมีความจำเป็น แต่เราอาจพยายามค้นหากรณีพิเศษบางอย่างที่คอมไพเลอร์สามารถใช้โครงร่างการจัดการหน่วยความจำที่ถูกกว่าการเก็บขยะ ตัวอย่างเช่นกำหนด
f :: Integer -> Integer
f x = let x2 = x*x in x2*x2
เราอาจหวังให้คอมไพลเลอร์ตรวจพบว่าx2
สามารถยกเลิกการจัดสรรได้อย่างปลอดภัยเมื่อf
ส่งคืน (แทนที่จะรอให้ตัวรวบรวมขยะยกเลิกการจัดสรรx2
) โดยพื้นฐานแล้วเรากำลังขอให้คอมไพเลอร์ทำการวิเคราะห์การหลบหนีเพื่อแปลงการปันส่วนในฮีปที่รวบรวมขยะเป็นการจัดสรรบนสแต็กทุกที่ที่ทำได้
นี่ไม่ใช่เรื่องเกินสมควรที่จะถาม: คอมไพเลอร์ jhc haskellทำสิ่งนี้แม้ว่า GHC จะไม่ทำก็ตาม Simon Marlow กล่าวว่าคนเก็บขยะในยุคของ GHC ทำให้การวิเคราะห์การหลบหนีไม่จำเป็นเป็นส่วนใหญ่
JHC จริงใช้เป็นรูปแบบที่มีความซับซ้อนของการวิเคราะห์การหลบหนีที่รู้จักกันในภูมิภาคอนุมาน พิจารณา
f :: Integer -> (Integer, Integer)
f x = let x2 = x * x in (x2, x2+1)
g :: Integer -> Integer
g x = case f x of (y, z) -> y + z
ในกรณีนี้การวิเคราะห์การหลบหนีอย่างง่ายจะสรุปได้ว่าการx2
หลบหนีจากf
(เนื่องจากถูกส่งกลับในทูเพิล) และด้วยเหตุนี้จึงx2
ต้องจัดสรรให้กับกองขยะที่เก็บรวบรวม ในทางกลับกันการอนุมานภูมิภาคสามารถตรวจพบว่าx2
สามารถจัดสรรได้เมื่อg
ส่งคืน แนวคิดในที่นี้คือx2
ควรจัดสรรในg
ภูมิภาคมากกว่าf
ภูมิภาค
แม้ว่าการอนุมานภูมิภาคจะมีประโยชน์ในบางกรณีตามที่กล่าวไว้ข้างต้น แต่ดูเหมือนว่าจะเป็นการยากที่จะกระทบยอดอย่างมีประสิทธิภาพด้วยการประเมินแบบขี้เกียจ (ดูความคิดเห็นของ Edward KmettและSimon Peyton Jones ) ตัวอย่างเช่นพิจารณา
f :: Integer -> Integer
f n = product [1..n]
อาจถูกล่อลวงให้จัดสรรรายการ[1..n]
บนสแต็กและยกเลิกการจัดสรรหลังจากf
ส่งคืน แต่สิ่งนี้จะเป็นหายนะ: จะเปลี่ยนf
จากการใช้หน่วยความจำ O (1) (ภายใต้การรวบรวมขยะ) เป็นหน่วยความจำ O (n)
งานที่กว้างขวางเกิดขึ้นในปี 1990 และต้นปี 2000 ในการอนุมานภูมิภาคสำหรับML ภาษาที่ใช้งานได้อย่างเข้มงวด Mads ทอฟเตลาร์ส Birkedal มาร์ติน Elsman นีลส์ Hallenberg ได้เขียนอ่านได้ค่อนข้างย้อนหลังในการทำงานของพวกเขาในภูมิภาคอนุมานมากที่พวกเขารวมอยู่ในคอมไพเลอร์ MLKit พวกเขาทดลองกับการจัดการหน่วยความจำตามภูมิภาคอย่างหมดจด (เช่นไม่มีตัวเก็บขยะ) รวมถึงการจัดการหน่วยความจำแบบไฮบริดตามภูมิภาค / ที่เก็บรวบรวมขยะและรายงานว่าโปรแกรมทดสอบของพวกเขาทำงาน "เร็วกว่า 10 เท่าและช้ากว่า 4 เท่า" มากกว่าขยะบริสุทธิ์ รุ่นที่รวบรวม
Nothing
) ไปยังการเรียกซ้ำloop
และยกเลิกการจัดสรรรายการเก่า - ไม่ทราบอายุการใช้งาน แน่นอนว่าไม่มีใครต้องการการใช้งาน Haskell แบบไม่แชร์เพราะมันช้ามากสำหรับโครงสร้างข้อมูลขนาดใหญ่
ลองมาเป็นตัวอย่างเล็กน้อย ระบุสิ่งนี้
f (x, y)
คุณต้องการที่จะจัดสรรทั้งคู่อยู่ที่ไหนสักแห่งก่อนที่จะเรียก(x, y)
f
คุณสามารถยกเลิกการจัดสรรคู่นั้นได้เมื่อใด คุณไม่มีความคิด ไม่สามารถยกเลิกการจัดสรรได้เมื่อf
ส่งคืนเนื่องจากf
อาจจะใส่คู่ในโครงสร้างข้อมูล (เช่นf p = [p]
) f
ดังนั้นชีวิตของทั้งคู่อาจจะต้องมีความยาวเกินกว่าที่กลับมาจาก ตอนนี้บอกว่าทั้งคู่ถูกจัดให้อยู่ในรายชื่อใครจะสามารถแยกรายการออกจากกันได้หรือไม่? ไม่เพราะอาจมีการแชร์ทั้งคู่ (เช่นlet p = (x, y) in (f p, p)
) ดังนั้นจึงยากที่จะบอกได้ว่าเมื่อใดที่สามารถยกเลิกการจัดสรรทั้งคู่ได้
เช่นเดียวกันสำหรับการจัดสรรเกือบทั้งหมดใน Haskell ที่กล่าวว่าเป็นไปได้ที่จะมีการวิเคราะห์ (การวิเคราะห์ภูมิภาค) ที่ให้ขอบเขตสูงสุดเกี่ยวกับอายุการใช้งาน สิ่งนี้ใช้ได้ดีพอสมควรในภาษาที่เข้มงวด แต่น้อยกว่าในภาษาขี้เกียจ (ภาษาขี้เกียจมักจะมีการกลายพันธุ์มากกว่าภาษาที่เข้มงวดในการใช้งาน)
ดังนั้นฉันต้องการเปลี่ยนคำถาม ทำไมคุณถึงคิดว่า Haskell ไม่ต้องการ GC คุณจะแนะนำการจัดสรรหน่วยความจำอย่างไร?
สัญชาตญาณของคุณที่ว่าสิ่งนี้เกี่ยวข้องกับความบริสุทธิ์มีความจริงบางอย่าง
Haskell ถือว่าบริสุทธิ์ส่วนหนึ่งเนื่องจากผลข้างเคียงของฟังก์ชั่นถูกนำมาพิจารณาในลายเซ็นประเภท ดังนั้นหากฟังก์ชันมีผลข้างเคียงจากการพิมพ์บางสิ่งต้องมีไฟล์IO
บางอย่างในประเภทการส่งคืน
แต่มีฟังก์ชั่นที่ใช้โดยปริยายทุกที่ใน Haskell และลายเซ็นประเภทที่ไม่ได้อธิบายไว้ในแง่หนึ่งคือผลข้างเคียง ได้แก่ ฟังก์ชันที่คัดลอกข้อมูลบางส่วนและให้คุณกลับมาสองเวอร์ชัน ภายใต้ประทุนนี้สามารถทำงานได้อย่างแท้จริงโดยการทำซ้ำข้อมูลในหน่วยความจำหรือ 'แทบ' โดยการเพิ่มหนี้ที่ต้องชำระคืนในภายหลัง
เป็นไปได้ที่จะออกแบบภาษาที่มีระบบประเภทที่ จำกัด ยิ่งขึ้น (ระบบ "เชิงเส้น" อย่างเดียว) ที่ไม่อนุญาตฟังก์ชันการคัดลอก จากมุมมองของโปรแกรมเมอร์ในภาษาดังกล่าว Haskell ดูไม่บริสุทธิ์เล็กน้อย
ในความเป็นจริงCleanซึ่งเป็นญาติของ Haskell มีประเภทเชิงเส้น (อย่างเคร่งครัดมากขึ้น: ไม่ซ้ำกัน) และสามารถให้ความคิดได้ว่าการไม่อนุญาตให้คัดลอกเป็นอย่างไร แต่ Clean ยังอนุญาตให้คัดลอกสำหรับประเภท "ไม่ซ้ำกัน" ได้
มีงานวิจัยมากมายในพื้นที่นี้และหากคุณ Google เพียงพอคุณจะพบตัวอย่างของโค้ดเชิงเส้นที่ไม่จำเป็นต้องเก็บขยะ คุณจะพบระบบทุกประเภทที่สามารถส่งสัญญาณไปยังคอมไพเลอร์ว่าหน่วยความจำใดที่อาจใช้เพื่อให้คอมไพเลอร์กำจัด GC บางส่วนได้
มีความรู้สึกที่อัลกอริทึมควอนตัมเป็นเส้นตรงอย่างแท้จริง ทุกการดำเนินการสามารถย้อนกลับได้ดังนั้นจึงไม่สามารถสร้างคัดลอกข้อมูลได้หรือทำลายข้อมูลได้ (มันเป็นเส้นตรงตามความหมายทางคณิตศาสตร์ตามปกติด้วย)
นอกจากนี้ยังน่าสนใจที่จะเปรียบเทียบกับ Forth (หรือภาษาที่ใช้สแต็กอื่น ๆ ) ซึ่งมีการดำเนินการ DUP อย่างชัดเจนซึ่งทำให้เกิดความชัดเจนเมื่อเกิดการทำซ้ำ
อื่น ๆ (นามธรรมมากขึ้น) วิธีคิดเกี่ยวกับเรื่องนี้คือการทราบว่า Haskell diag :: X -> (X, X)
ถูกสร้างขึ้นจากเพียงแค่พิมพ์แลมบ์ดาแคลคูลัสซึ่งอยู่บนพื้นฐานทฤษฎีของประเภทปิดคาร์ทีเซียนและที่ประเภทเช่นมาพร้อมกับฟังก์ชั่นในแนวทแยง ภาษาที่อิงตามคลาสของหมวดหมู่อื่นอาจไม่มีสิ่งนั้น
แต่โดยทั่วไปการเขียนโปรแกรมเชิงเส้นอย่างหมดจดนั้นยากเกินกว่าที่จะเป็นประโยชน์ดังนั้นเราจึงตัดสินใจเลือก GC
เทคนิคการใช้งานมาตรฐานที่ใช้กับ Haskell จำเป็นต้องใช้ GC มากกว่าภาษาอื่น ๆ ส่วนใหญ่เนื่องจากไม่เคยเปลี่ยนค่าก่อนหน้านี้แทนที่จะสร้างค่าใหม่ที่แก้ไขตามค่าก่อนหน้า เนื่องจากนี่หมายความว่าโปรแกรมมีการจัดสรรและใช้หน่วยความจำมากขึ้นอย่างต่อเนื่องค่าจำนวนมากจะถูกทิ้งเมื่อเวลาผ่านไป
นี่คือเหตุผลที่โปรแกรม GHC มักจะมีตัวเลขการจัดสรรรวมที่สูงเช่นนี้ (ตั้งแต่กิกะไบต์ไปจนถึงเทราไบต์): พวกเขาจัดสรรหน่วยความจำอย่างต่อเนื่องและต้องขอบคุณ GC ที่มีประสิทธิภาพเท่านั้นที่พวกเขาเรียกคืนก่อนที่จะหมด
หากภาษา (ภาษาใดก็ได้) อนุญาตให้คุณจัดสรรวัตถุแบบไดนามิกมีสามวิธีที่ใช้ได้จริงในการจัดการกับการจัดการหน่วยความจำ:
ภาษาสามารถอนุญาตให้คุณจัดสรรหน่วยความจำบนสแตกหรือเมื่อเริ่มต้นเท่านั้น แต่ข้อ จำกัด เหล่านี้ จำกัด ประเภทของการคำนวณที่โปรแกรมสามารถทำได้อย่างรุนแรง (ในทางปฏิบัติในทางทฤษฎีคุณสามารถเลียนแบบโครงสร้างข้อมูลแบบไดนามิกใน (พูด) Fortran โดยการแทนค่าในอาร์เรย์ขนาดใหญ่มันน่ากลัว ... และไม่เกี่ยวข้องกับการสนทนานี้)
ภาษาสามารถให้ชัดเจนfree
หรือdispose
กลไก แต่นี่ต้องอาศัยโปรแกรมเมอร์เพื่อทำให้ถูกต้อง ความผิดพลาดใด ๆ ในการจัดการพื้นที่เก็บข้อมูลอาจทำให้หน่วยความจำรั่ว ... หรือแย่กว่านั้น
ภาษา (หรือมากกว่านั้นคือการนำภาษาไปใช้อย่างเคร่งครัด) สามารถจัดเตรียมตัวจัดการหน่วยเก็บข้อมูลอัตโนมัติสำหรับหน่วยเก็บข้อมูลที่จัดสรรแบบไดนามิก เช่นคนเก็บขยะบางรูปแบบ
ทางเลือกเดียวคือห้ามเรียกคืนพื้นที่เก็บข้อมูลที่จัดสรรแบบไดนามิก นี่ไม่ใช่วิธีแก้ปัญหาที่ใช้ได้จริงยกเว้นโปรแกรมขนาดเล็กที่ทำการคำนวณขนาดเล็ก
การนำสิ่งนี้ไปใช้กับ Haskell ภาษาไม่มีข้อ จำกัด คือ 1. และไม่มีการดำเนินการยกเลิกการจัดสรรด้วยตนเองตามข้อ 2 ดังนั้นเพื่อให้สามารถใช้งานได้กับสิ่งที่ไม่สำคัญการใช้งาน Haskell จำเป็นต้องรวมตัวเก็บขยะ .
ฉันไม่สามารถนึกถึงกรณีที่ GC จำเป็นในภาษาที่บริสุทธิ์
สันนิษฐานว่าคุณหมายถึงภาษาที่ใช้งานได้จริง
คำตอบคือต้องใช้ GC ภายใต้ประทุนเพื่อเรียกคืนฮีปอ็อบเจ็กต์ที่ภาษาต้องสร้างขึ้น ตัวอย่างเช่น.
ฟังก์ชันบริสุทธิ์จำเป็นต้องสร้างวัตถุฮีปเนื่องจากในบางกรณีจะต้องส่งคืนวัตถุเหล่านั้น นั่นหมายความว่าไม่สามารถจัดสรรบนสแตกได้
ความจริงที่ว่าอาจมีรอบได้ (เป็นผลมาจากlet rec
ตัวอย่าง) หมายความว่าวิธีการนับอ้างอิงจะใช้ไม่ได้กับฮีปอ็อบเจ็กต์
จากนั้นจะมีการปิดฟังก์ชัน ... ซึ่งไม่สามารถจัดสรรบนสแต็กได้เนื่องจากมีอายุการใช้งานที่ (โดยทั่วไป) เป็นอิสระจากสแต็กเฟรมที่สร้างขึ้น
ฉันกำลังมองหาโค้ดตัวอย่างที่อาจรั่วไหลหากไม่มี GC
ตัวอย่างที่เกี่ยวข้องกับการปิดหรือโครงสร้างข้อมูลรูปกราฟก็จะรั่วไหลภายใต้เงื่อนไขเหล่านั้น
ไม่จำเป็นต้องมีคนเก็บขยะหากคุณมีหน่วยความจำเพียงพอ อย่างไรก็ตามในความเป็นจริงเราไม่มีหน่วยความจำที่ไม่มีที่สิ้นสุดดังนั้นเราจึงต้องการวิธีการบางอย่างในการเรียกคืนหน่วยความจำที่ไม่จำเป็นอีกต่อไป ในภาษาที่ไม่บริสุทธิ์เช่น C คุณสามารถระบุได้อย่างชัดเจนว่าคุณทำเสร็จแล้วโดยใช้หน่วยความจำบางส่วนเพื่อปลดปล่อย - แต่นี่เป็นการดำเนินการที่กลายพันธุ์ (หน่วยความจำที่คุณเพิ่งปลดปล่อยจะไม่ปลอดภัยในการอ่านอีกต่อไป) ดังนั้นคุณจึงไม่สามารถใช้แนวทางนี้ได้ ภาษาที่บริสุทธิ์ ดังนั้นจึงเป็นการวิเคราะห์แบบคงที่ว่าคุณสามารถเพิ่มหน่วยความจำได้ที่ไหน (อาจเป็นไปไม่ได้ในกรณีทั่วไป) หน่วยความจำรั่วเช่นตะแกรง (ใช้งานได้ดีจนกว่าคุณจะหมด) หรือใช้ GC
GC "ต้องมี" ในภาษา FP ที่แท้จริง ทำไม? การดำเนินการจัดสรรและฟรีไม่บริสุทธิ์! และเหตุผลประการที่สองคือโครงสร้างข้อมูลแบบวนซ้ำที่ไม่เปลี่ยนรูปนั้นต้องการ GC เพื่อการดำรงอยู่เนื่องจากการเชื่อมโยงย้อนกลับสร้างโครงสร้างที่เป็นนามธรรมและไม่สามารถเข้าถึงได้สำหรับจิตใจมนุษย์ แน่นอนว่าการลิงก์ย้อนกลับเป็นประโยชน์อย่างยิ่งเพราะการคัดลอกโครงสร้างที่ใช้มันมีราคาถูกมาก
อย่างไรก็ตามหากคุณไม่เชื่อฉันเพียงแค่ลองใช้ภาษา FP แล้วคุณจะเห็นว่าฉันพูดถูก
แก้ไข: ฉันลืม ความเกียจคร้านเป็นนรกที่ไม่มี GC ไม่เชื่อฉัน? เพียงแค่ลองโดยไม่ต้องใช้ GC ในตัวอย่างเช่น C ++ คุณจะเห็น ... สิ่งต่างๆ
Haskell เป็นภาษาการเขียนโปรแกรมที่ไม่เข้มงวด แต่การใช้งานส่วนใหญ่ใช้การเรียกตามความต้องการ (ความเกียจคร้าน) เพื่อใช้งานที่ไม่เข้มงวด ในการเรียกใช้ตามความต้องการคุณจะประเมินสิ่งต่างๆเมื่อถึงระหว่างรันไทม์โดยใช้เครื่องจักร "thunks" เท่านั้น (นิพจน์ที่รอการประเมินแล้วเขียนทับตัวเองโดยยังคงมองเห็นค่าของมันเพื่อนำกลับมาใช้เมื่อจำเป็น)
ดังนั้นหากคุณใช้ภาษาของคุณอย่างเกียจคร้านโดยใช้ thunks คุณได้เลื่อนการใช้เหตุผลทั้งหมดเกี่ยวกับอายุการใช้งานของวัตถุไปจนถึงช่วงเวลาสุดท้ายซึ่งเป็นรันไทม์ เนื่องจากตอนนี้คุณไม่รู้อะไรเลยเกี่ยวกับช่วงชีวิตสิ่งเดียวที่คุณทำได้อย่างสมเหตุสมผลคือการเก็บขยะ ...