Bu fibonacci işlevi nasıl hafızaya alınır?


114

Bu fibonacci işlevi hangi mekanizma ile hafızaya alınır?

fib = (map fib' [0..] !!)                 
     where fib' 1 = 1                                                        
           fib' 2 = 1                                                        
           fib' n = fib (n-2) + fib (n-1)                    

Ve ilgili bir kayda göre, bu sürüm neden değil?

fib n = (map fib' [0..] !! n)                                               
     where fib' 1 = 1                                                        
           fib' 2 = 1                                                        
           fib' n = fib (n-2) + fib (n-1)                    

13
Biraz unrelatedly, fib 0sona ermiyor: muhtemelen baz davaları istemediği fib'olmak fib' 0 = 0ve fib' 1 = 1.
huon

1
İlk sürümün daha özlü yapılabileceğini unutmayın: fibs = 1:1:zipWith (+) fibs (tail fibs)ve fib = (fibs !!).
Bastian

Yanıtlar:


95

Haskell'deki değerlendirme mekanizması ihtiyaca bağlıdır : bir değere ihtiyaç duyulduğunda hesaplanır ve tekrar istenmesi durumunda hazır tutulur. Bir liste tanımlarsak xs=[0..]ve daha sonra 100. elemanını xs!!99sorarsak, listedeki 100. yuva, bir 99sonraki erişime hazır olan numarayı şimdi tutarak "açıklanır" .

Bu, "listeden geçme" hilesinin istismar ettiği şeydir. Normal çift yinelemeli Fibonacci tanımında, fib n = fib (n-1) + fib (n-2)işlevin kendisi üstten iki kez çağrılır ve üstel patlamaya neden olur. Ancak bu numara ile, ara sonuçlar için bir liste hazırlıyoruz ve "listeyi gözden geçiriyoruz":

fib n = (xs!!(n-1)) + (xs!!(n-2)) where xs = 0:1:map fib [2..]

İşin püf noktası, bu listenin oluşturulmasına neden olmak ve bu listenin, aramalar arasında (çöp toplama yoluyla) kaybolmamasına neden olmaktır fib. Bunu başarmak için en kolay yolu, etmektir isim o listeyi. "Adını verirsen, kalacak."


İlk sürümünüz bir monomorfik sabit tanımlar ve ikincisi bir polimorfik işlevi tanımlar. Bir polimorfik işlev, sunması gereken farklı türler için aynı dahili listeyi kullanamaz, bu nedenle paylaşım yok , yani not alma yok.

İlk sürümde, derleyici bize karşı cömert davranıyor , bu sabit alt ifadeyi ( map fib' [0..]) çıkarıyor ve onu ayrı bir paylaşılabilir varlık haline getiriyor, ancak bunu yapma yükümlülüğü altında değil. ve biz durumu aslında orada yok otomatik bizim için bunu yapmak ister.

( edit :) Şu yeniden yazımları düşünün:

fib1 = f                     fib2 n = f n                 fib3 n = f n          
 where                        where                        where                
  f i = xs !! i                f i = xs !! i                f i = xs !! i       
  xs = map fib' [0..]          xs = map fib' [0..]          xs = map fib' [0..] 
  fib' 1 = 1                   fib' 1 = 1                   fib' 1 = 1          
  fib' 2 = 1                   fib' 2 = 1                   fib' 2 = 1          
  fib' i=fib1(i-2)+fib1(i-1)   fib' i=fib2(i-2)+fib2(i-1)   fib' i=f(i-2)+f(i-1)

Yani gerçek hikaye, iç içe geçmiş kapsam tanımlarıyla ilgili görünüyor. 1. tanımda dış kapsam yoktur ve 3. dış kapsamı fib3değil, aynı düzeyi çağırmaya dikkat eder f.

Her yeni çağırma fib2bunlardan herhangi nedeniyle yeniden kendi iç içe tanımlarını oluşturmak gibi görünüyor olabilir (teoride) farklı tanımlanabileceğini bağlı değerine n(yani out işaret için Vitus ve Tikhon sayesinde). İlk Definition ile hiçbir orada nbağlıdır ve üçüncü bir bağımlılık, bunlarla her bir ayrı çağrı var fib3içine aramaların fbu özel çağırma iç aynı düzey kapsamından sadece çağrı tanımları, dikkatli olan fib3aynı nedenle, xsalır bu çağrı için yeniden kullanıldı (yani paylaşıldı) fib3.

Ancak hiçbir şey, derleyicinin yukarıdaki sürümlerin herhangi birindeki dahili tanımların aslında dış bağlamadan bağımsız olduğunu fark etmesini engellemez , sonuçta lambda kaldırmayın gerçekleştirir ve sonuçta tam not alma ile sonuçlanır (polimorfik tanımlar hariç). Aslında, monomorfik türlerle bildirildiğinde ve -O2 bayrağıyla derlendiğinde her üç sürümde de olan tam olarak budur. Polimorfik tip bildirimleri ile yerel paylaşım sergiler ve hiç paylaşım yoktur.fib3fib2

Nihayetinde, kullanılan bir derleyiciye ve derleyici optimizasyonlarına ve onu nasıl test ettiğinize (GHCI'de, derlenmiş veya değil, -O2 ile veya değil veya bağımsız olarak dosya yükleme) ve monomorfik veya polimorfik bir tip alıp almadığına bağlı olarak davranış tamamen değiştirin - yerel (arama başına) paylaşım (yani her aramada doğrusal zaman), not alma (yani ilk aramada doğrusal zaman ve aynı veya daha küçük bağımsız değişkenle sonraki aramalarda 0 kez) veya hiç paylaşım içermesin ( üstel zaman).

Kısa cevap, bu bir derleyici şeydir. :)


4
Sadece küçük bir ayrıntıyı düzeltmek için: İkinci versiyon yerel işlevi esas olarak, çünkü herhangi bir paylaşımı almaz fib'her için yeniden tanımlanır ndolayısıyla ve fib'de fib 1fib'içinde fib 2de listeler farklıdır anlamına gelen. Türü monomorfik olarak ayarlasanız bile, bu davranışı yine de sergiler.
Vitus

1
wherecümlecikler paylaşıma çok benzer letifadeler getirir, ancak bunun gibi sorunları gizleme eğilimindedirler. Biraz daha açık bir şekilde yeniden yazarsak, şunu elde edersiniz: hpaste.org/71406
Vitus

1
Yeniden yazmanızla ilgili bir başka ilginç nokta: Onlara monomorfik tip (yani Int -> Integer) verirseniz fib2, üstel zamanda çalışır fib1ve fib3her ikisi de doğrusal zamanda çalışır, ancak fib1aynı zamanda hafızaya alınır - çünkü fib3yerel tanımlar her biri için yeniden tanımlanır n.
Vitus

1
@misterbee Ama gerçekten de derleyiciden bir tür güvence almak güzel olurdu; belirli bir varlığın bellek yerleşimi üzerinde bir tür kontrol. Bazen paylaşmak isteriz, bazen engellemek isteriz. Bunun mümkün olmasını hayal ediyorum / umuyorum ...
Will Ness

1
@ElizaBrandt demek istediğim, bazen ağır bir şeyi yeniden hesaplamak istediğimizdir, bu yüzden bizim için hafızada tutulmaz - yani yeniden hesaplama maliyeti, büyük bellek tutma maliyetinden daha düşüktür. bir örnek, güç kümesi oluşturma: pwr (x:xs) = pwr xs ++ map (x:) pwr xs ; pwr [] = [[]]biz pwr xsbağımsız olarak iki kez hesaplanmak istiyoruz , böylece üretilirken ve tüketilirken anında çöp toplanabilir.
Will Ness

23

Tam olarak emin değilim, ama işte mantıklı bir tahmin:

Derleyici fib n, bunun farklı bir yerde farklı olabileceğini nve bu nedenle listeyi her seferinde yeniden hesaplaması gerektiğini varsayar . Sonuçta , whereifadenin içindeki bitler buna bağlı olabilirn . Yani, bu durumda, sayıların tam listesi aslında bir işlevidir n.

Olmayan sürüm n, listeyi bir kez oluşturabilir ve bir fonksiyona sarabilir. Liste ,n geçirilen değerin değerine bağlı olamaz ve bunun doğrulanması kolaydır. Liste, daha sonra indekslenen bir sabittir. Elbette tembel olarak değerlendirilen bir sabittir, bu nedenle programınız tüm (sonsuz) listeyi hemen almaya çalışmaz. Sabit olduğu için fonksiyon çağrıları arasında paylaşılabilir.

Yinelemeli çağrının sadece bir listede bir değer araması gerektiği için hatırlanır. Yana fibversiyon kez tembel listesini oluşturur, sadece gereksiz hesaplama yapmadan cevap almak için yeterli hesaplar. Burada "tembel", listedeki her girişin bir thunk (değerlendirilmemiş bir ifade) olduğu anlamına gelir. Ne zaman do thunk değerlendirmek, o kadar doğru hiç hesaplamasını tekrar bir sonraki sefer erişen bir değer haline gelir. Liste aramalar arasında paylaşılabildiğinden, önceki tüm girişler bir sonrakine ihtiyaç duyduğunuz zamana göre zaten hesaplanmıştır.

Esasen, GHC'nin tembel semantiğine dayanan akıllı ve düşük maliyetli bir dinamik programlama biçimidir. Bunun olması gerektiğini standart tek belirliyorsa düşünüyorum olmayan sıkı bir uyumlu derleyici potansiyel için bu kodu derlemek böylece, değil memoize. Bununla birlikte, pratikte her makul derleyici tembel olacaktır.

İkinci vakanın neden işe yaradığı hakkında daha fazla bilgi için, okuyun için Özyinelemeli olarak tanımlanmış bir listeyi anlama (zipWith cinsinden fibs) başlıklı makaleyi okuyun .


belki " fib' nfarklı bir yerde farklı olabilir " mi demek istediniz n?
Will Ness

Sanırım çok net değildim: demek istediğim, içerideki her şey fibdahil fib', her biri farklı olabilirdi n. Bence orijinal örnek biraz kafa karıştırıcı çünkü fib'aynı zamanda ndiğerini gölgeleyen kendine de bağlı n.
Tikhon Jelvis

20

İlk olarak, ghc-7.4.2 ile derlenmiş -O2 , hafızaya alınmamış sürüm o kadar kötü değildir, Fibonacci numaralarının dahili listesi, işleve her üst düzey çağrı için hala hafızaya alınır. Ancak, farklı üst düzey çağrılarda hafızaya alınmaz ve makul olarak yapılamaz. Ancak, diğer sürüm için liste aramalar arasında paylaşılır.

Bu, monomorfizm kısıtlamasından kaynaklanmaktadır.

İlki, basit bir örüntü bağlamasıyla bağlıdır (yalnızca ad, argüman yok), bu nedenle monomorfizm kısıtlamasıyla monomorfik bir tür alması gerekir. Çıkarılan tür

fib :: (Num n) => Int -> n

ve böyle bir kısıtlama Integer, türü şu şekilde sabitleyerek ( aksi söylenen bir varsayılan bildirimin yokluğunda) varsayılan olur .

fib :: Int -> Integer

Böylece [Integer]hatırlanacak tek bir liste (türden ) vardır.

İkincisi bir fonksiyon argümanı ile tanımlanır, bu nedenle polimorfik kalır ve eğer dahili listeler aramalar arasında hafızaya alınmışsa, her tür için bir listenin hafızaya alınması gerekir. Num . Bu pratik değil.

Her iki sürümü de monomorfizm kısıtlaması devre dışı bırakılmış olarak veya aynı tür imzaları ile derleyin ve her ikisi de tam olarak aynı davranışı sergiler. (Bu eski derleyici sürümleri için doğru değildi, bunu hangi sürümün ilk yaptığını bilmiyorum.)


Her tür için bir listeyi ezberlemek neden pratik değildir? Prensipte, GHC, çalışma zamanı sırasında karşılaşılan her Num türü için kısmen hesaplanmış listeler içeren bir sözlük (türü sınıfla sınırlı işlevleri çağırmak gibi) oluşturabilir mi?
misterbee

1
@misterbee Prensipte olabilir, ancak program fib 1000000birçok türü çağırırsa , bu çok fazla bellek tüketir . Bundan kaçınmak için, çok büyüdüğünde önbellekten atılacak bir sezgisel listeye ihtiyaç vardır. Ve böyle bir ezberleme stratejisi, muhtemelen diğer işlevler veya değerler için de geçerli olacaktır, bu nedenle derleyicinin, potansiyel olarak birçok tür için ezberlemek için potansiyel olarak çok sayıda şeyle uğraşması gerekecektir. Oldukça iyi bir buluşsal yöntemle (kısmi) çok biçimli hafızayı uygulamanın mümkün olacağını düşünüyorum, ancak bunun zahmete değeceğinden şüpheliyim.
Daniel Fischer

5

Haskell için memoize fonksiyonuna ihtiyacınız yok. Yalnızca deneysel programlama dili bu işlevlere ihtiyaç duyar. Bununla birlikte, Haskel işlevsel bir dildir ve ...

İşte bu çok hızlı Fibonacci algoritmasına bir örnektir:

fib = zipWith (+) (0:(1:fib)) (1:fib)

zipWith, standart Prelude'un işlevidir:

zipWith :: (a->b->c) -> [a]->[b]->[c]
zipWith op (n1:val1) (n2:val2) = (n1 + n2) : (zipWith op val1 val2)
zipWith _ _ _ = []

Ölçek:

print $ take 100 fib

Çıktı:

[1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368,75025,121393,196418,317811,514229,832040,1346269,2178309,3524578,5702887,9227465,14930352,24157817,39088169,63245986,102334155,165580141,267914296,433494437,701408733,1134903170,1836311903,2971215073,4807526976,7778742049,12586269025,20365011074,32951280099,53316291173,86267571272,139583862445,225851433717,365435296162,591286729879,956722026041,1548008755920,2504730781961,4052739537881,6557470319842,10610209857723,17167680177565,27777890035288,44945570212853,72723460248141,117669030460994,190392490709135,308061521170129,498454011879264,806515533049393,1304969544928657,2111485077978050,3416454622906707,5527939700884757,8944394323791464,14472334024676221,23416728348467685,37889062373143906,61305790721611591,99194853094755497,160500643816367088,259695496911122585,420196140727489673,679891637638612258,1100087778366101931,1779979416004714189,2880067194370816120,4660046610375530309,7540113804746346429,12200160415121876738,19740274219868223167,31940434634990099905,51680708854858323072,83621143489848422977,135301852344706746049,218922995834555169026,354224848179261915075,573147844013817084101]

Geçen süre: 0.00018s


Bu çözüm harika!
Larry
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.