İşlevsel programlamada, değişken yapıların çoğuna sahip olmak daha fazla bellek kullanımı gerektiriyor mu?


63

İşlevsel programlamada, neredeyse tüm veri yapıları değiştirilemez olduğundan, durumun yeni bir yapıyı değiştirmesi gerektiğinde değişmezdir. Bu çok daha fazla bellek kullanımı anlamına mı geliyor? Nesneye yönelik programlama paradigmasını iyi biliyorum, şimdi işlevsel programlama paradigmasını öğrenmeye çalışıyorum. Değişmez olan her şeyin kavramı beni şaşırtıyor. Değişmez yapıları kullanan bir programın değişken yapıları olan bir programdan çok daha fazla bellek gerektirdiği anlaşılıyor. Buna doğru şekilde mi bakıyorum?


7
Bu , anlamına gelmez, ancak çoğu değişmez veri yapısı, değişikliklerin altında yatan verileri yeniden kullanır. Eric Lippert'ın hakkında harika bir blog dizisi vardır C # değişmezlik
Oded

3
Tamamen İşlevsel Veri Yapıları'na göz atacağım, Haskell'in konteyner kütüphanesinin çoğunu yazan aynı kişi tarafından yazılmış harika bir kitap (kitap öncelikle SML olsa da)
jozefg

1
Bellek tüketimi yerine çalışma süresiyle ilgili bu cevap sizin için de ilginç olabilir: stackoverflow.com/questions/1990464/…
9000

Yanıtlar:


35

Bunun tek doğru cevabı "bazen" dir. Hafızayı boşa harcamamak için fonksiyonel dillerin kullanabileceği birçok püf noktası vardır. İmkansızlık, derleyici verilerinin değiştirilmeyeceğini garanti edebileceğinden, fonksiyonlar arasında ve hatta veri yapıları arasında veri paylaşımını kolaylaştırır. İşlevsel diller, değişken yapılar olarak verimli bir şekilde kullanılabilecek veri yapılarının kullanımını teşvik etme eğilimindedir (örneğin karma tablolar yerine ağaçlar). Karışıma tembellik eklerseniz, birçok işlevsel dilde olduğu gibi, bu bellek tasarrufu için yeni yollar ekler (aynı zamanda belleği boşa harcamanın yeni yollarını da ekler ancak buna girmeyeceğim).


24

İşlevsel programlamada, neredeyse tüm veri yapıları değiştirilemez olduğundan, durumun yeni bir yapıyı değiştirmesi gerektiğinde değişmezdir. Bu çok daha fazla bellek kullanımı anlamına mı geliyor?

Bu, veri yapısına, yaptığınız değişikliklerin ve bazı durumlarda optimize ediciye bağlıdır. Bir örnek olarak, bir listeye hazır olmayı düşünelim:

list2 = prepend(42, list1) // list2 is now a list that contains 42 followed
                           // by the elements of list1. list1 is unchanged

Burada ek bellek gereksinimi sabittir - yani çalışma zamanı maliyeti prepend. Neden? Çünkü prependbasitçe 42başı ve list1kuyruğu olan yeni bir hücre yaratır . Bunu list2başarmak için kopyalamak veya başka şekilde yinelemek zorunda değildir . Bu saklamak için gerekli olan bellek hariç olup 42, list2kullanılan aynı hafıza yeniden kullanır list1. Her iki liste de değişmez olduğundan, bu paylaşım tamamen güvenlidir.

Benzer şekilde, dengeli ağaç yapılarıyla çalışırken, çoğu işlem yalnızca logaritmik miktarda ek alan gerektirir, çünkü her şey, ancak ağacın bir yolu paylaşılabilir.

Diziler için durum biraz farklı. Bu nedenle, çoğu FP dilinde, diziler yaygın şekilde kullanılmamaktadır. Bununla birlikte, eğer böyle bir şey yaparsanız arr2 = map(f, arr1)ve arr1bu satırdan sonra bir daha hiç kullanılmazsa, akıllı bir iyileştirici aslında arr1yeni bir dizi oluşturmak yerine mutasyona uğrayan bir kod oluşturabilir (programın davranışını etkilemeden). Bu durumda, performans elbette zorunlu bir dilde olacaktır.


1
İlgi alanı dışında, hangi dillerin uygulanmasını sonuna kadar tanımladığınız alanı yeniden kullanıyorsunuz?

@delnan Üniversitemde bunu yapan Qube adında bir araştırma dili vardı. Yine de, bunu yapan herhangi bir vahşi dil kullanılıp kullanılmadığını bilmiyorum. Ancak Haskell'in füzyonu birçok durumda aynı etkiyi sağlayabilir.
sepp2k

7

Naif uygulamalar gerçekten de bu sorunu ortaya çıkarır - mevcut bir yerinde güncellemek yerine yeni bir veri yapısı oluşturduğunuzda, bir miktar ek yükünüz olmalıdır.

Farklı dillerin bununla başa çıkmanın farklı yolları vardır ve çoğunun kullandığı birkaç püf noktası vardır.

Bir strateji çöp toplama . Yeni yapının yaratıldığı andan kısa bir süre sonra eski yapıya yapılan atıflar kapsam dışına çıkar ve çöp toplayıcı, GC algoritmasına bağlı olarak hemen veya en kısa sürede toparlayacaktır. Bu, hala bir ek yük varken, yalnızca geçici olduğu ve veri miktarıyla doğrusal olarak büyümeyeceği anlamına gelir.

Bir diğeri, farklı türde veri yapılarını seçmek. Dizilerin zorunluluk dillerinde listelenecek veri yapısı olduğu yerlerde (genellikle std::vectorC ++ 'da olduğu gibi bir çeşit dinamik yeniden yerleştirme kabına sarılır ), işlevsel diller genellikle bağlantılı listeleri tercih eder. Bağlantılı bir listeyle, bir hazırlık işlemi ('eksileri') mevcut listeyi yeni listenin kuyruğu olarak yeniden kullanabilir, bu yüzden gerçekten tahsis edilen tek şey yeni liste başkanıdır. Diğer veri yapıları türleri için de benzer stratejiler var - kümeler, ağaçlar, siz adlandırın.

Ve sonra tembel bir değerlendirme var, la Haskell. Fikir, oluşturduğunuz veri yapılarının derhal tam olarak yaratılmadığı; bunun yerine, "tıkaçlar" olarak depolanırlar (bunları gerektiğinde değeri oluşturmak için tarifler olarak düşünebilirsiniz). Sadece değere ihtiyaç duyulduğunda thunk gerçek bir değere genişler. Bu, değerlendirme gerekene kadar bellek tahsisinin ertelenebileceği ve bu noktada birkaç bellek bir bellek tahsisinde birleştirilebileceği anlamına gelir.


Vay, bir küçük cevap ve çok fazla bilgi / içgörü. Teşekkür ederim :)
Gerry

3

Ben sadece Clojure hakkında biraz bilgim var ve bu değişmez Veri Yapıları .

Clojure bir dizi değişmez liste, vektör, set ve harita sunar. Değiştirilemediklerinden, değişmeyen bir koleksiyondan bir şey 'ekleme' veya 'kaldırma', eskisi gibi ancak ihtiyaç duyulan değişiklikle yeni bir koleksiyon oluşturma anlamına gelir. Kalıcılık, koleksiyonun eski versiyonunun 'değişiklikten sonra hala mevcut olduğu ve koleksiyonun çoğu işlem için performans garantisini koruduğu özelliği tanımlamak için kullanılan bir terimdir. Spesifik olarak, bu, yeni sürümün tam bir kopya kullanarak oluşturulamayacağı anlamına gelir, çünkü doğrusal zaman gerektirir. Kaçınılmaz olarak, kalıcı koleksiyonlar bağlı veri yapıları kullanılarak gerçekleştirilir, böylece yeni sürümler yapıyı önceki sürümle paylaşabilir.

Grafik olarak, şöyle bir şeyi temsil edebiliriz:

(def my-list '(1 2 3))

    +---+      +---+      +---+
    | 1 | ---> | 2 | ---> | 3 |
    +---+      +---+      +---+

(def new-list (conj my-list 0))

              +-----------------------------+
    +---+     | +---+      +---+      +---+ |
    | 0 | --->| | 1 | ---> | 2 | ---> | 3 | |
    +---+     | +---+      +---+      +---+ |
              +-----------------------------+

2

Diğer cevaplarda söylenenlere ek olarak, eşsiz türleri destekleyen Temiz programlama dilinden de bahsetmek istiyorum . Bu dili bilmiyorum ama benzersiz türlerin bir tür "yıkıcı güncelleştirmeyi" desteklediğini düşünüyorum.

Başka bir deyişle, bir durumu güncelleme semantiği, bir işlevi uygulayarak eskisinden yeni bir değer yaratıyor olmanıza rağmen, benzersizlik kısıtı, derleyicinin veri nesnelerini dahili olarak yeniden kullanmasına izin verebilir çünkü eski değerin referans alınmayacağını bilir. Programda yeni değer üretildikten sonra artık.

Daha fazla ayrıntı için, bkz . Temiz ana sayfa ve bu wikipedia makalesi.

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.