Haskell bir çöp toplayıcıya ihtiyaç duyar mı?


119

Haskell uygulamalarının neden bir GC kullandığını merak ediyorum.

Saf bir dilde GC'nin gerekli olduğu bir durum düşünemiyorum. Kopyalamayı azaltmak için sadece bir optimizasyon mu yoksa gerçekten gerekli mi?

Bir GC yoksa sızıntı yapacak örnek kod arıyorum.


15
Bu seriyi aydınlatıcı bulabilirsiniz; çöplerin nasıl üretildiğini (ve ardından toplandığını) kapsar: blog.ezyang.com/2011/04/the-haskell-heap
Tom Crockett

6
her yerde saf dillerde referanslar var! sadece değiştirilebilir referanslar değil .
Tom Crockett

1
@pelotom Değişmez verilere veya değişmez referanslara referanslar?
Pubby

3
Her ikisi de. Bahsedilen verilerin değişmez olması gerçeği, tüm referansların tamamen değişmez olmasından kaynaklanmaktadır.
Tom Crockett

4
Bu akıl yürütmeyi bellek tahsisine uygulamak , genel durumda serbest bırakmanın neden statik olarak tahmin edilemediğini anlamaya yardımcı olduğundan , durdurma problemiyle kesinlikle ilgileneceksiniz . Bununla birlikte, serbest bırakmanın tahmin edilebileceği bazı programlar vardır , tıpkı onları gerçekten çalıştırmadan sonlandırdığı bilinen bazı programlar gibi .
Paul R

Yanıtlar:


218

Başkalarının da belirttiği gibi, Haskell otomatik , dinamik bellek yönetimi gerektirir : otomatik bellek yönetimi gereklidir çünkü manuel bellek yönetimi güvensizdir; dinamik bellek yönetimi gereklidir çünkü bazı programlar için bir nesnenin ömrü yalnızca çalışma zamanında belirlenebilir.

Örneğin, aşağıdaki programı düşünün:

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

Bu programda liste [1..1000], kullanıcı "temizle" yazana kadar bellekte tutulmalıdır; bu nedenle bu yaşam boyu olmalıdır dinamik belirlenecek ve dinamik bellek yönetimi neden gerekli olduğunu budur.

Dolayısıyla bu anlamda, otomatik dinamik bellek tahsisi gereklidir ve pratikte bunun anlamı: evet , Haskell bir çöp toplayıcı gerektirir, çünkü çöp toplama en yüksek performanslı otomatik dinamik bellek yöneticisidir.

Ancak...

Bir çöp toplayıcı gerekli olsa da, derleyicinin çöp toplamadan daha ucuz bir bellek yönetimi şeması kullanabileceği bazı özel durumlar bulmaya çalışabiliriz. Örneğin, verilen

f :: Integer -> Integer
f x = let x2 = x*x in x2*x2

Derleyicinin x2, fgeri döndüğünde (çöp toplayıcının serbest bırakmasını beklemek yerine) güvenli bir şekilde ayrılabileceğini algılamasını umabiliriz x2. Esasen, derleyicinin ayırmaları çöpte toplanan yığına, mümkün olduğunda yığın üzerindeki ayırmalara dönüştürmek için kaçış analizi gerçekleştirmesini istiyoruz.

Bunu istemek çok mantıksız değildir: jhc haskell derleyicisi bunu yapar, ancak GHC yapmaz. Simon Marlow , GHC'nin nesilsel çöp toplayıcısının kaçış analizini çoğunlukla gereksiz hale getirdiğini söylüyor .

jhc aslında bölge çıkarımı olarak bilinen sofistike bir kaçış analizi kullanır . Düşünmek

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

Bu durumda, basit bir kaçış analizi, x2kaçışların f(demet içinde döndürüldüğü için) olduğu sonucuna varır ve bu nedenle x2çöp toplama yığınına tahsis edilmesi gerekir. Öte yandan bölge çıkarımı, geri döndüğünde x2ayrılabilecek olanı tespit edebilir g; buradaki fikir, bunun bölgesinden ziyade bölgesine x2tahsis edilmesi gerektiğidir .gf

Haskell'in ötesinde

Yukarıda tartışıldığı gibi bazı durumlarda bölge çıkarımı yararlı olsa da, tembel değerlendirmeyle etkili bir şekilde uzlaştırmak zor görünmektedir ( Edward Kmett ve Simon Peyton Jones'un yorumlarına bakınız). Örneğin, düşünün

f :: Integer -> Integer
f n = product [1..n]

Listeyi [1..n]yığına ayırmak ve fgeri döndükten sonra onu serbest bırakmak cazip gelebilir , ancak bu felaket olur: fO (1) belleğini (çöp toplama altında) kullanmaktan O (n) belleğine değişir .

Katı işlevsel dil ML'si için bölge çıkarımı konusunda 1990'larda ve 2000'lerin başında kapsamlı çalışmalar yapıldı . Mads Tofte, Lars Birkedal, Martin Elsman, Niels Hallenberg , çoğu MLKit derleyicisine entegre ettikleri bölge çıkarımı konusundaki çalışmaları üzerine oldukça okunabilir bir geriye dönük yazmışlardır . Tamamen bölge tabanlı bellek yönetimi (yani çöp toplayıcı yok) ve ayrıca hibrit bölge tabanlı / çöp toplanan bellek yönetimi ile deneyler yaptılar ve test programlarının saf çöpten "10 kat daha hızlı ve 4 kat daha yavaş" çalıştığını bildirdi. toplanan sürümler.


2
Haskell'in paylaşılması gerekiyor mu? Değilse, ilk örneğinizde, listenin bir kopyasını (yanıt Nothing) loopeskisinin özyinelemeli çağrısına geçirebilir ve eski olanı serbest bırakabilirsiniz - bilinmeyen yaşam süresi yok. Elbette hiç kimse Haskell'in paylaşılmayan bir uygulamasını istemez, çünkü büyük veri yapıları için korkunç derecede yavaştır.
nimi

3
Tek kafa karışıklığım ilk örnek olmakla birlikte bu cevabı gerçekten çok beğendim. Açıkçası, eğer kullanıcı asla "temizle" yazmadıysa, o zaman sonsuz bellek kullanabilir (GC olmadan), ancak bellek hala izlendiğinden bu tam olarak bir sızıntı sayılmaz.
Pubby

3
C ++ 11, akıllı işaretçilerin harika bir uygulamasına sahiptir. Temelde referans sayımı kullanır. Sanırım Haskell benzer bir şey lehine çöp toplamayı bırakabilir ve bu nedenle determinist olabilir.
intrepidis

3
@ChrisNash - Çalışmıyor. Akıllı işaretçiler, başlık altında referans sayma kullanır. Referans sayma, döngüleri olan veri yapılarını ele alamaz. Haskell, döngülerle veri yapıları oluşturabilir.
Stephen C

3
Bu cevabın dinamik bellek ayırma kısmına katılıp katılmadığımdan emin değilim. Programın, bir kullanıcının geçici olarak döngüyü ne zaman durduracağını bilmemesi, onu dinamik yapmamalıdır. Bu, derleyicinin bir şeyin bağlam dışına çıkıp çıkmayacağını bilip bilmediğine göre belirlenir. Haskell'in durumunda, bunun resmi olarak dil grameri tarafından tanımlandığı yerde, yaşam bağlamı bilinir. Bununla birlikte, liste ifadelerinin ve türünün dil içinde dinamik olarak üretilmesi nedeniyle bellek yine de dinamik olabilir.
Timothy Swan

27

Önemsiz bir örnek alalım. Bu göz önüne alındığında

f (x, y)

(x, y)aramadan önce çifti bir yere ayırmanız gerekir f. Bu çifti ne zaman serbest bırakabilirsiniz? Hiçbir fikrin yok. Döndüğünde ayrılması kaldırılamaz f, çünkü fçifti bir veri yapısına (örneğin f p = [p]) koymuş olabilir , bu nedenle çiftin yaşam süresinin dönüşten daha uzun olması gerekebilir f. Şimdi, çiftin bir listeye konduğunu söyleyin, listeyi kim ayırırsa, çifti ayırabilir mi? Hayır, çünkü çift paylaşılabilir (örneğin, let p = (x, y) in (f p, p)). Bu nedenle, çiftin ayrılmasının ne zaman kaldırılabileceğini söylemek gerçekten zor.

Aynı durum Haskell'deki hemen hemen tüm tahsisler için de geçerlidir. Bununla birlikte, yaşam süresine ilişkin bir üst sınır veren bir analize (bölge analizi) sahip olmak mümkündür. Bu, katı dillerde oldukça iyi çalışır, ancak tembel dillerde daha az işe yarar (tembel diller, uygulamada katı dillerden çok daha fazla mutasyon yapma eğilimindedir).

Bu yüzden soruyu tersine çevirmek istiyorum. Haskell'in neden GC'ye ihtiyacı olmadığını düşünüyorsunuz? Bellek ayırmanın yapılmasını nasıl önerirsiniz?


18

Bunun saflıkla bir ilgisi olduğuna dair sezginiz, biraz da doğrudur.

Haskell, kısmen tip imzasında işlevlerin yan etkilerinin hesaba katılması nedeniyle saf olarak kabul edilir. Dolayısıyla, bir işlevin bir şeyi yazdırmak gibi bir yan etkisi varsa IO, dönüş türünde bir yer olmalıdır.

Ancak Haskell'de her yerde örtük olarak kullanılan ve tip imzası bir anlamda yan etkiyi açıklamayan bir işlev var. Yani bazı verileri kopyalayan ve size iki sürümü geri veren işlev. Kaputun altında bu, hafızadaki verileri kopyalayarak ya da daha sonra geri ödenmesi gereken bir borcu artırarak 'sanal olarak' çalışabilir.

Kopyalama işlevine izin vermeyen daha da kısıtlayıcı tip sistemlere (tamamen "doğrusal" olanlar) sahip diller tasarlamak mümkündür. Böyle bir dildeki bir programcının bakış açısından Haskell biraz saf görünmüyor.

Aslında, Haskell'in bir akrabası olan Clean , doğrusal (daha kesin olarak: benzersiz) türlere sahiptir ve bu, kopyalamaya izin vermemenin nasıl bir şey olduğu konusunda bir fikir verebilir. Ancak Temiz, "benzersiz olmayan" türler için kopyalamaya yine de izin verir.

Bu alanda pek çok araştırma var ve eğer yeterince Google olursanız, çöp toplama gerektirmeyen saf doğrusal kod örnekleri bulacaksınız. Derleyiciye hangi belleğin kullanılabileceğini bildirebilen ve derleyicinin bazı GC'leri ortadan kaldırmasına olanak tanıyan her türden sistemi bulacaksınız.

Kuantum algoritmalarının da tamamen doğrusal olduğu bir anlam var. Her işlem tersine çevrilebilir ve bu nedenle hiçbir veri oluşturulamaz, kopyalanamaz veya yok edilemez . (Ayrıca her zamanki matematiksel anlamda doğrusaldırlar.)

Ayrıca, çoğaltma gerçekleştiğinde netleşen açık DUP işlemlerine sahip Forth (veya diğer yığın tabanlı diller) ile karşılaştırmak ilginçtir.

Bunun hakkında düşünmenin bir başka (daha soyut) yolu, Haskell'in, kartezyen kapalı kategoriler teorisine dayanan basit bir şekilde yazılmış lambda hesabından oluştuğunu ve bu tür kategorilerin köşegen bir işlevle donatıldığını not etmektir diag :: X -> (X, X). Başka bir kategori sınıfına dayalı bir dilin böyle bir şeyi olmayabilir.

Ancak genel olarak, tamamen doğrusal programlama yararlı olamayacak kadar zordur, bu nedenle GC'ye razı oluruz.


3
Bu cevabı yazdığımdan beri Rust programlama dilinin popülaritesi epeyce arttı. Bu nedenle, Rust'un belleğe erişimi kontrol etmek için doğrusal-benzeri bir sistem kullandığından bahsetmeye değer ve pratikte kullanıldığından bahsettiğim fikirleri görmek istiyorsanız bir göz atmaya değer.
sigfpe

14

Haskell'e uygulanan standart uygulama teknikleri aslında diğer birçok dilden daha fazla GC gerektirir, çünkü önceki değerleri asla değiştirmezler, bunun yerine öncekilere dayanan yeni, değiştirilmiş değerler oluştururlar. Bu, programın sürekli olarak daha fazla bellek ayırdığı ve kullandığı anlamına geldiğinden, zaman geçtikçe çok sayıda değer atılacaktır.

Bu nedenle, GHC programları bu kadar yüksek toplam tahsis rakamlarına (gigabayttan terabayta kadar) sahip olma eğilimindedir: sürekli bellek ayırırlar ve tükenmeden önce onu geri kazanmaları yalnızca verimli GC sayesinde olur.


2
"önceki değerleri asla değiştirmezler": haskell.org/haskellwiki/HaskellImplementorsWorkshop/2011/Takano'ya bakabilirsiniz , bu, belleği yeniden kullanan deneysel bir GHC uzantısı ile ilgilidir.
gfour

11

Bir dil (herhangi bir dil) nesneleri dinamik olarak ayırmanıza izin veriyorsa, bellek yönetimi ile başa çıkmanın üç pratik yolu vardır:

  1. Dil, yalnızca yığın üzerinde veya başlangıçta bellek ayırmanıza izin verebilir. Ancak bu kısıtlamalar, bir programın gerçekleştirebileceği hesaplama türlerini ciddi şekilde sınırlar. (Pratikte. Teoride, Fortran'daki dinamik veri yapılarını büyük bir dizide temsil ederek taklit edebilirsiniz. Bu KORKUNÇ ... ve bu tartışma ile ilgili değil.)

  2. Dil, bir açık freeveya disposemekanizma sağlayabilir . Ancak bu, programcının doğru yapmasına bağlıdır. Depolama yönetimindeki herhangi bir hata bellek sızıntısına veya daha kötüsüne neden olabilir.

  3. Dil (veya daha kesin olarak dil uygulaması), dinamik olarak ayrılmış depolama için otomatik bir depolama yöneticisi sağlayabilir; yani bir çeşit çöp toplayıcı.

Diğer tek seçenek, dinamik olarak ayrılmış depolamayı asla geri almamaktır. Bu, küçük hesaplamalar yapan küçük programlar dışında pratik bir çözüm değildir.

Bunu Haskell'e uyguladığımızda, dilin 1 sınırlaması yoktur ve 2'ye göre manuel ayırma işlemi yoktur. Bu nedenle, önemsiz olmayan şeyler için kullanılabilir olması için bir Haskell uygulamasının bir çöp toplayıcı içermesi gerekir. .

Saf bir dilde GC'nin gerekli olduğu bir durum düşünemiyorum.

Muhtemelen saf bir işlevsel dili kastediyorsunuz.

Cevap, dilin yaratması ZORUNLU olduğu yığın nesnelerini geri almak için başlık altında bir GC'nin gerekli olmasıdır. Örneğin.

  • Saf bir işlevin yığın nesneleri yaratması gerekir çünkü bazı durumlarda onları geri döndürmesi gerekir. Bu, yığın üzerinde ayrılamayacakları anlamına gelir.

  • Döngülerin olabileceği gerçeği ( let recörneğin bir sonuçtan kaynaklanmaktadır ), bir referans sayma yaklaşımının yığın nesneleri için işe yaramayacağı anlamına gelir.

  • Daha sonra, yaratıldıkları yığın çerçevesinden (tipik olarak) bağımsız bir ömürleri olduğu için yığın üzerinde tahsis edilemeyen işlev kapanışları vardır.

Bir GC yoksa sızıntı yapacak örnek kod arıyorum.

Kapanışları veya grafik şekilli veri yapılarını içeren hemen hemen her örnek bu koşullar altında sızıntı yapacaktır.


2
Neden seçenekler listenizin kapsamlı olduğunu düşünüyorsunuz? Amaç C'de ARC, MLKit ve DDC'de bölge çıkarımı, Mercury'de derleme zamanı çöp toplama - hepsi bu listeye uymuyor.
Dee Pzt

@DeeMon - hepsi bu kategorilerden birine uyuyor. Yapmadıklarını düşünüyorsanız, bunun nedeni kategori sınırlarını çok sıkı çizmenizdir. "Bir çeşit çöp toplama" dediğimde, depolamanın otomatik olarak geri kazanıldığı herhangi bir mekanizmayı kastediyorum .
Stephen C

1
C ++ 11, akıllı işaretçiler kullanır. Temelde referans sayımı kullanır. Belirleyici ve otomatiktir. Haskell'in bir uygulamasının bu yöntemi kullandığını görmek isterim.
intrepidis

2
@ChrisNash - 1) İşe yaramaz. Döngüleri bozmak için uygulama koduna güvenemediğiniz sürece, referans sayım tabanlı ıslah, döngü varsa verileri sızdırır. 2) Referans saymanın modern (gerçek) bir çöp toplayıcıyla karşılaştırıldığında kötü performans gösterdiği (bunları inceleyen kişilerce) iyi bilinir.
Stephen C

@DeeMon - ayrıca Reinerp'in Haskell ile bölge çıkarımının neden pratik olmayacağına dair cevabına bakın.
Stephen C

8

Yeterli belleğiniz olduğu sürece bir çöp toplayıcıya asla gerek yoktur. Bununla birlikte, gerçekte, sonsuz hafızamız yok ve bu yüzden artık ihtiyaç duyulmayan hafızayı geri kazanmak için bir yönteme ihtiyacımız var. C gibi saf olmayan dillerde, hafızayı boşaltmak için biraz hafıza ile işinizin bittiğini açıkça belirtebilirsiniz - ancak bu bir mutasyon işlemidir (az önce serbest bıraktığınız hafızanın okunması artık güvenli değildir), bu nedenle bu yaklaşımı kullanamazsınız saf bir dil. Yani ya bir şekilde statik olarak nerede hafızayı boşaltabileceğinizi analiz edin (genel durumda muhtemelen imkansız), bir elek gibi hafızayı sızdırın (siz bitene kadar harika çalışıyor) ya da bir GC kullanın.


Bu, genel olarak GC'nin neden gereksiz olduğunu yanıtlıyor, ancak özellikle Haskell ile daha çok ilgileniyorum.
Pubby

10
Genel olarak bir GC teorik olarak gereksizse, o zaman önemsiz bir şekilde Haskell için teorik olarak gereksiz olduğu anlamına gelir.
ehird

@ehird Gerekli demek istedim, yazım denetleyicimin anlamını ters yüz ettiğini düşünüyorum.
Pubby

1
Ehird yorumu hala geçerli :-)
Paul R

2

GC, saf FP dillerinde "olmalıdır" dır. Neden? Tahsis edilen ve ücretsiz işlemler saf değildir! Ve ikinci neden, değişmez özyinelemeli veri yapılarının varolmak için GC'ye ihtiyaç duymasıdır çünkü geri bağlantı, insan zihni için karmaşık ve sürdürülemez yapılar oluşturur. Elbette backlink yapmak bir nimet çünkü onu kullanan yapıların kopyalanması çok ucuz.

Neyse, bana inanmıyorsanız, sadece FP dilini uygulamaya çalışın ve haklı olduğumu göreceksiniz.

DÜZENLEME: Unuttum. Tembellik GC'siz Cehennemdir. Bana inanma Örneğin, C ++ 'da GC olmadan deneyin. Göreceksin ... şeyler


1

Haskell katı olmayan bir programlama dilidir, ancak çoğu uygulama, katı olmamayı uygulamak için ihtiyaca göre çağrı (tembellik) kullanır. İhtiyaca göre çağrıda, yalnızca çalışma sırasında "thunks" mekanizmasını kullanarak ulaşılan şeyleri değerlendirirsiniz (değerlendirilmeyi bekleyen ve sonra kendi üzerine yazan ifadeler, gerektiğinde yeniden kullanılabilmesi için görünür kalarak).

Öyleyse, dilinizi thunks kullanarak tembel bir şekilde uygularsanız, nesne yaşamları hakkındaki tüm muhakemeleri son ana, yani çalışma zamanına ertelemiş olursunuz. Artık yaşamlar hakkında hiçbir şey bilmediğiniz için, makul olarak yapabileceğiniz tek şey çöp toplamaktır ...


1
Bazı durumlarda statik analiz, thunk değerlendirildikten sonra bazı verileri serbest bırakan thunks kodunu ekleyebilir. Serbest bırakma, çalışma zamanında gerçekleşir, ancak bu GC değildir. Bu, C ++ 'daki akıllı işaretçileri referans sayma fikrine benzer. Nesne yaşam süreleri hakkında akıl yürütme, çalışma zamanında gerçekleşir, ancak GC kullanılmaz.
Dee Pzt
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.