GHC Haskell'de not alma ne zaman otomatiktir?


106

M2 aşağıda olmadığı halde m1'in neden hafızaya alındığını anlayamıyorum:

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

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

m1 10000000, ilk aramada yaklaşık 1,5 saniye ve sonraki aramalarda bunun bir kısmını alır (muhtemelen listeyi önbelleğe alır), oysa m2 10000000 her zaman aynı süreyi alır (her aramada listeyi yeniden oluşturmak). Neler olduğu hakkında bir fikrin var mı? GHC'nin bir işlevi hatırlayıp hatırlamayacağına ve ne zaman hatırlayacağına dair pratik kurallar var mı? Teşekkürler.

Yanıtlar:


112

GHC, fonksiyonları hafızaya almaz.

Bununla birlikte, kodda verilen herhangi bir ifadeyi, çevreleyen lambda ifadesinin girildiği anda en fazla bir kez veya en üst düzeydeyse en fazla bir kez hesaplar. Lambda ifadelerinin nerede olduğunu belirlemek, örneğinizdeki gibi sözdizimsel şekeri kullandığınızda biraz zor olabilir, bu yüzden bunları eşdeğer desugared sözdizimine çevirelim:

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

(Not: Haskell 98 raporu aslında bir sol operatör bölümünü (a %)eşdeğer olarak \b -> (%) a btanımlıyor, ancak GHC bunu tasarlıyor (%) a. Bunlar teknik olarak farklı çünkü ayırt edilebiliyorlar seq. Sanırım bununla ilgili bir GHC Trac bileti göndermiş olabilirim.)

Bu göz önüne alındığında, söz konusu in görebilirsiniz m1'ifade filter odd [1..]sadece programın vadede başına bir kez bilgisayarlı olacak böylece iken, herhangi bir lambda-ifadesinde yer almayan m2', filter odd [1..]lambda-ifadesi girildiğinde her zaman bilgisayarlı olacak yani her çağrıda m2'. Bu gördüğünüz zamanlamadaki farkı açıklıyor.


Aslında, belirli optimizasyon seçeneklerine sahip bazı GHC sürümleri, yukarıdaki açıklamanın gösterdiğinden daha fazla değeri paylaşacaktır. Bu, bazı durumlarda sorunlu olabilir. Örneğin, işlevi düşünün

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

GHC y, bunun bağlı olmadığını fark edebilir xve işlevi yeniden yazar.

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

Bu durumda, yeni sürüm, ydepolandığı yerden yaklaşık 1 GB okumak zorunda kalacağı için , orijinal sürüm sabit alanda çalışacak ve işlemcinin önbelleğine sığacağı için çok daha az verimlidir . Aslında, GHC 6.12.1 altında, işlev f, optimizasyon yapılmadan derlendiğinde derlendiğinden neredeyse iki kat daha hızlıdır -O2.


1
(Tek [1 ..] filtreleme) ifadesini değerlendirme maliyeti zaten sıfıra yakındır - sonuçta tembel bir listedir, bu nedenle liste fiilen değerlendirildiğinde gerçek maliyet (x !! 10000000) uygulamasındadır. Ayrıca, hem m1 hem de m2, en azından aşağıdaki testte -O2 ve -O1 (benim ghc 6.12.3'ümde) ile yalnızca bir kez değerlendirilebilir: (test = m1 10000000 seqm1 10000000). Optimizasyon bayrağı belirtilmediğinde bir fark vardır. Ve bu arada, "f" nin her iki varyantı da optimizasyondan bağımsız olarak 5356 baytlık maksimum ikametgahına sahiptir (-O2 kullanıldığında daha az toplam tahsis ile).
Ed'ka

1
Ed'ka @ yukarıdaki tanımı ile bu test programı deneyin f: main = interact $ unlines . (show . map f . read) . lines; ile veya olmadan derleme -O2; sonra echo 1 | ./main. Bir test gibi yazarsanız main = print (f 5), o zaman ykullanıldığı şekilde çöp toplandı ve iki arasında hiçbir fark yoktur olabilir fs.
Reid Barton

map (show . f . read)Tabii ki olmalı . Ve şimdi GHC 6.12.3'ü indirdiğime göre, GHC 6.12.1'deki ile aynı sonuçları görüyorum. Ve evet, haklısın orijinal üzeresiniz m1ve m2değiştirecek etkin optimizasyonlarla kaldırma bu tür gerçekleştirmek ghc sürümleri: m2içine m1.
Reid Barton

Evet, şimdi farkı görüyorum (-O2 kesinlikle daha yavaştır). Bu örnek için teşekkürler!
Ed'ka

29

m1, Sabit Başvuru Formu olduğu için yalnızca bir kez hesaplanırken, m2 CAF değildir ve bu nedenle her değerlendirme için hesaplanır.

CAF'lerdeki GHC wiki'ye bakın: http://www.haskell.org/haskellwiki/Constant_applicative_form


1
“M1, Sabit Başvuru Formu olduğu için yalnızca bir kez hesaplanıyor” açıklaması bana mantıklı gelmiyor. Muhtemelen hem m1 hem de m2 üst düzey değişkenler olduğundan, bu işlevlerin CAF olup olmadıklarına bakılmaksızın yalnızca bir kez hesaplandığını düşünüyorum . Aradaki fark, listenin [1 ..]bir programın yürütülmesi sırasında yalnızca bir kez hesaplanması veya işlevin her uygulaması için bir kez hesaplanmasıdır, ancak bu CAF ile ilgili mi?
Tsuyoshi Ito

1
Bağlantılı sayfadan: "Bir CAF ... ya tüm kullanıcılar tarafından paylaşılacak bir grafik parçasına ya da ilk değerlendirildiğinde bir grafikle kendi üzerine yazacak paylaşılan bir koda derlenebilir". Yana m1bir CAF, ikincisi de geçerlidir ve filter odd [1..](sadece [1..]!) Yalnızca bir kez hesaplanır. GHC , bunun aynı düşünceye m2atıfta bulunduğunu filter odd [1..]ve buna bir bağlantı verdiğini de not edebilir m1, ancak bu kötü bir fikir olur: bazı durumlarda büyük bellek sızıntılarına yol açabilir.
Alexey Romanov

@Alexey: [1..]ve hakkındaki düzeltmeler için teşekkür ederim filter odd [1..]. Geri kalanı için hala ikna olmadım. Yanılmıyorsam Eğer bir derleyici iddia istediğinizde, CAF sadece ilgili olduğu olabilirdi yerine filter odd [1..]de m2(kullanılan biri olarak hatta aynı thunk olabilecek küresel bir thunk tarafından m1). Ama asker Nickli Üyenin durumda, derleyici vermedi değil “optimizasyonu” bunu ben soruya alaka göremiyorum.
Tsuyoshi Ito

2
Onun yerini ki alakalıdır içinde m1 ve öyle.
Alexey Romanov

13

İki form arasında çok önemli bir fark vardır: monomorfizm kısıtlaması m1 için geçerlidir, ancak m2'ye uygulanmaz, çünkü m2 açıkça argümanlar vermiştir. Yani m2'nin türü geneldir, ancak m1'ler özeldir. Atanan türler şunlardır:

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

Çoğu Haskell derleyicisi ve yorumlayıcısı (aslında bildiğim hepsi) polimorfik yapıları ezberlemiyor, bu nedenle m2'nin dahili listesi her çağrıldığında, m1'in olmadığı yerde yeniden oluşturulur.


1
Bunlarla GHCi'de oynamak, aynı zamanda serbest yüzen dönüşüme de bağlı gibi görünüyor (GHC'nin GHCi'de kullanılmayan optimizasyon geçişlerinden biri). Ve tabii ki bu basit fonksiyonları derlerken, optimize edici onların yine de aynı şekilde davranmalarını sağlayabilir (yine de çalıştırdığım bazı kriter testlerine göre, fonksiyonlar ayrı bir modülde ve NOINLINE pragmalarıyla işaretlenmiştir). Muhtemelen bunun nedeni liste oluşturma ve indekslemenin yine de süper sıkı bir döngü içinde kaynaşmasıdır.
mokus

1

Emin değilim, çünkü Haskell'de oldukça yeniyim, ancak görünen o ki, ikinci işlev parametreleştirilmiş ve birincisi değil. Fonksiyonun doğası, sonucunun girdi değerine ve özellikle işlevsel paradigmada YALNIZCA girdiye bağlı olmasıdır. Açıkça çıkarım, parametresi olmayan bir fonksiyonun, ne olursa olsun her zaman aynı değeri tekrar tekrar döndürmesidir.

Görünüşe göre GHC derleyicisinde, bu gerçeği tüm program çalışma süresi için yalnızca bir kez böyle bir işlevin değerini hesaplamak için kullanan optimize edici bir mekanizma vardır. Kesinlikle tembelce yapıyor, ama yine de yapıyor. Aşağıdaki işlevi yazdığımda bunu kendim fark ettim:

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

Sonra bunu test etmek, ben GHCi girdi ve şöyle yazdı: primes !! 1000. Bir kaç saniye sürdü, ama sonunda cevap aldım: 7927. Sonra aradım primes !! 1001ve anında cevap aldım. Benzer şekilde bir anda sonucunu aldım take 1000 primes, çünkü Haskell daha önce 1001. elementi döndürmek için tüm bin element listesini hesaplamak zorunda kaldı.

Bu nedenle, fonksiyonunuzu hiçbir parametre almayacak şekilde yazabiliyorsanız, muhtemelen istersiniz. ;)

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.