Fonksiyonel programlama - değişmezlik pahalı mı? [kapalı]


98

Soru iki bölümden oluşuyor. İlki kavramsaldır. Bir sonraki adım aynı soruya Scala'da daha somut bir şekilde bakıyor.

  1. Bir programlama dilinde yalnızca değişmez veri yapılarının kullanılması, belirli algoritmaların / mantığın uygulanmasını pratikte hesaplama açısından daha pahalı hale getirir mi? Bu, değişmezliğin tamamen işlevsel dillerin temel ilkelerinden biri olduğu gerçeğini ortaya çıkarır. Bunu etkileyen başka faktörler var mı?
  2. Daha somut bir örnek alalım. Quicksort genellikle bir bellek içi veri yapısı üzerinde değiştirilebilir işlemler kullanılarak öğretilir ve uygulanır. Böyle bir şey, değiştirilebilir versiyona kıyasla karşılaştırılabilir bir hesaplama ve depolama ek yükü ile PURE işlevsel bir şekilde nasıl uygulanır? Özellikle Scala'da. Aşağıda bazı kaba ölçütler ekledim.

Daha fazla detay:

Zorunlu programlama geçmişinden geliyorum (C ++, Java). Fonksiyonel programlamayı, özellikle de Scala'yı araştırıyorum.

Saf fonksiyonel programlamanın temel ilkelerinden bazıları:

  1. Fonksiyonlar birinci sınıf vatandaşlardır.
  2. Fonksiyonların yan etkileri yoktur ve bu nedenle nesneler / veri yapıları değişmezdir .

Modern JVM'ler nesne oluşturma konusunda son derece verimli olsalar ve kısa ömürlü nesneler için çöp toplama çok ucuz olsa da , nesne oluşturmayı en aza indirmek muhtemelen daha iyidir, değil mi? En azından eşzamanlılık ve kilitlemenin sorun olmadığı tek iş parçacıklı bir uygulamada. Scala hibrit bir paradigma olduğu için, gerekirse değiştirilebilir nesnelerle zorunlu kod yazmayı seçebiliriz. Ancak, nesneleri yeniden kullanmak ve tahsisi en aza indirmek için çok fazla yıl harcamış biri olarak. Buna bile izin vermeyecek düşünce okulunun iyi bir şekilde anlaşılmasını isterim.

Özel bir durum olarak, bu öğretici 6'daki bu kod parçacığı beni biraz şaşırttı . Quicksort'un Java sürümüne ve ardından aynı düzgün görünümlü Scala uygulamasına sahiptir.

İşte benim uygulamaları kıyaslama girişimim. Ayrıntılı profilleme yapmadım. Ama benim tahminim Scala sürümünün daha yavaş olduğu, çünkü tahsis edilen nesnelerin sayısı doğrusaldır (özyineleme çağrısı başına bir tane). Kuyruk arama optimizasyonlarının devreye girme şansı var mı? Haklıysam, Scala kendi kendini yinelemeli aramalar için kuyruk arama optimizasyonlarını destekler. Yani sadece ona yardım ediyor olmalı. Scala 2.8 kullanıyorum.

Java sürümü

public class QuickSortJ {

    public static void sort(int[] xs) {
      sort(xs, 0, xs.length -1 );
    }

    static void sort(int[] xs, int l, int r) {
      if (r >= l) return;
      int pivot = xs[l];
      int a = l; int b = r;
      while (a <= b){
        while (xs[a] <= pivot) a++;
        while (xs[b] > pivot) b--;
        if (a < b) swap(xs, a, b);
      }
      sort(xs, l, b);
      sort(xs, a, r);
    }

    static void swap(int[] arr, int i, int j) {
      int t = arr[i]; arr[i] = arr[j]; arr[j] = t;
    }
}

Scala versiyonu

object QuickSortS {

  def sort(xs: Array[Int]): Array[Int] =
    if (xs.length <= 1) xs
    else {
      val pivot = xs(xs.length / 2)
      Array.concat(
        sort(xs filter (pivot >)),
        xs filter (pivot ==),
        sort(xs filter (pivot <)))
    }
}

Uygulamaları karşılaştırmak için Scala Kodu

import java.util.Date
import scala.testing.Benchmark

class BenchSort(sortfn: (Array[Int]) => Unit, name:String) extends Benchmark {

  val ints = new Array[Int](100000);

  override def prefix = name
  override def setUp = {
    val ran = new java.util.Random(5);
    for (i <- 0 to ints.length - 1)
      ints(i) = ran.nextInt();
  }
  override def run = sortfn(ints)
}

val benchImmut = new BenchSort( QuickSortS.sort , "Immutable/Functional/Scala" )
val benchMut   = new BenchSort( QuickSortJ.sort , "Mutable/Imperative/Java   " )

benchImmut.main( Array("5"))
benchMut.main( Array("5"))

Sonuçlar

Art arda beş çalıştırma için milisaniye cinsinden süre

Immutable/Functional/Scala    467    178    184    187    183
Mutable/Imperative/Java        51     14     12     12     12

10
Saf bir şekilde veya zorunlu diller için geliştirilmiş yöntemlerle uygulandığında pahalıdır. Akıllı bir derleyici (örneğin, bir Haskell derleyicisi olan GHC - ve Haskell'in yalnızca değişmez değerleri vardır) , değişkenliği kullanarak koda rakip olabilecek değişmezlik ve başarma performansından yararlanabilir. Söylemeye gerek yok, quicksort'un saf uygulaması hala köpek yavaştır çünkü diğer maliyetli şeylerin yanı sıra ağır özyineleme ve O(n)list concat'ı kullanır . Sözde kod sürümünden daha kısa olsa da;)

3
Harika, ilgili bir blog makalesi burada: blogs.sun.com/jrose/entry/larval_objects_in_the_vm , Java'ya ve işlevsel
sanal makine

2
Bu SO iş parçacığı, fonksiyonel programlamanın verimliliği ile ilgili birçok ayrıntılı tartışmaya sahiptir. stackoverflow.com/questions/1990464/… . Bilmek istediklerimin çoğunu yanıtlıyor.
smartnut007

5
Buradaki en saf şey, kıyaslamanızdır. Böyle bir kodla hiçbir şeyi kıyaslayamazsınız! Herhangi bir sonuca varmadan önce JVM'de kıyaslama yapmakla ilgili bazı makaleleri ciddi olarak okumalısınız ... JVM'nin siz çalıştırmadan önce kodunuzu JIT'lememiş olabileceğini biliyor muydunuz? Yığınınızı ilk ve maksimum boyutunuzu uygun şekilde ayarladınız mı (böylece JVM işlemlerinin daha fazla bellek istediği zamanı dikkate almayacaksınız?)? Hangi yöntemlerin derlendiğinin veya yeniden derlendiğinin farkında mısınız? GC'den haberdar mısınız? Bu koddan elde ettiğiniz sonuçlar kesinlikle hiçbir şey ifade etmiyor!
Bruno Reis

2
@userunknown Hayır, bu bildirim niteliğindedir. Zorunlu programlama "durumu komutlarla değiştirir" oysa işlevsel programlama "durumu değiştirmeyi önleyen" bildirim temelli bir programlama paradigmasıdır ( Wikipedia ). Yani, evet, işlevsel ve zorunlu iki tamamen farklı şeylerdir ve yazdığınız kod zorunlu değildir .
Brian McCutchon

Yanıtlar:


106

Burada uçuşan birkaç yanılgı olduğu için , bazı noktaları açıklığa kavuşturmak istiyorum.

  • “Yerinde” (ve quicksort olduğu yerinde gerçekten değil quicksort değil yerinde tanım gereği). En iyi durumda O (log n ), en kötü durumda ise O ( n ) olan özyinelemeli adım için yığın alanı biçiminde ek depolama gerektirir .

  • Diziler üzerinde çalışan işlevsel bir hızlı sıralama varyantı uygulamak, amacı geçersiz kılar. Diziler asla değişmez değildir.

  • Quicksort'un "düzgün" işlevsel uygulaması, değişmez listeleri kullanır. Elbette yerinde değil, ancak prosedürel yerinde sürümle aynı en kötü durum asimptotik çalışma zamanına ( O ( n ^ 2)) ve uzay karmaşıklığına ( O ( n )) sahip.

    Ortalama olarak, çalışma süresi hala yerinde varyantınki ile aynıdır ( O ( n log n )). Uzay karmaşıklığı yine de O ( n ) 'dir.


İşlevsel bir hızlı sıralama uygulamasının iki bariz dezavantajı vardır . Aşağıda, Haskell girişinden bu referans uygulamasını Haskell'de (Scala bilmiyorum…) ele alalım :

qsort []     = []
qsort (x:xs) = qsort lesser ++ [x] ++ qsort greater
    where lesser  = (filter (< x) xs)
          greater = (filter (>= x) xs)
  1. İlk dezavantaj , çok esnek olmayan pivot elemanının seçimidir . Modern hızlı sıralama uygulamalarının gücü, büyük ölçüde akıllı bir pivot seçimine dayanır ( Bentley ve diğerlerinin "Mühendislik bir sıralama işlevi" ile karşılaştırın ). Yukarıdaki algoritma bu bakımdan zayıftır ve bu da ortalama performansı önemli ölçüde düşürür.

  2. İkinci olarak, bu algoritma bir O ( n ) işlemi olan liste birleştirme (liste yapımı yerine) kullanır . Bu, asimptotik karmaşıklığı etkilemez, ancak ölçülebilir bir faktördür.

Üçüncü bir dezavantaj bir şekilde gizlidir: "yerinde" varyantın aksine, bu uygulama sürekli olarak listenin eksileri hücreleri için yığından bellek ister ve muhtemelen her yere bellek dağıtır. Sonuç olarak, bu algoritma çok zayıf bir önbellek konumuna sahiptir . Modern fonksiyonel programlama dillerindeki akıllı ayırıcıların bunu hafifletip hafifletemeyeceğini bilmiyorum - ancak modern makinelerde, önbellek atlamaları büyük bir performans katili haline geldi.


Sonuç nedir? Diğerlerinden farklı olarak, quicksort'un doğası gereği zorunlu olduğunu ve bu yüzden FP ortamında kötü performans gösterdiğini söyleyemem. Tam tersine, quicksort'un işlevsel bir algoritmanın mükemmel bir örneği olduğunu söyleyebilirim: kusursuz bir şekilde değişmez bir ortama dönüşür, asimptotik çalışma süresi ve uzay karmaşıklığı, prosedür uygulamasına eşittir ve hatta prosedürel uygulaması özyinelemeyi kullanır.

Ancak bu algoritma , değişmez bir alanla sınırlandırıldığında hala daha kötü performans gösteriyor. Bunun nedeni, algoritmanın, yalnızca diziler üzerinde verimli bir şekilde gerçekleştirilebilen çok sayıda (bazen düşük düzeyde) ince ayardan yararlanma özelliğine sahip olmasıdır. Quicksort'un saf bir açıklaması, tüm bu karmaşıklıkları (hem işlevsel hem de prosedür varyantında) gözden kaçırır.

"Bir sıralama işlevi tasarlamak" ı okuduktan sonra artık hızlı sıralamayı zarif bir algoritma olarak düşünemiyorum. Verimli bir şekilde uygulandığında, hantal bir karmaşa, bir mühendisin işi, bir sanatçının değil (mühendisliğin değerini düşürmek değil! Bunun kendi estetiği var).


Ancak, bu noktanın hızlı sıralama için özel olduğunu da belirtmek isterim. Her algoritma aynı türden düşük seviyeli ince ayarlara tabi değildir. Pek çok algoritma ve veri yapısı , değişmez bir ortamda gerçekten performans kaybı olmadan ifade edilebilir .

Ve değişmezlik, maliyetli kopyalar veya iş parçacıkları arası senkronizasyon ihtiyacını ortadan kaldırarak performans maliyetlerini bile düşürebilir .

Öyleyse, orijinal soruyu yanıtlamak gerekirse, " değişmezlik pahalı mı? ”- Özel bir hızlı sıralama durumunda, gerçekten de değişmezliğin bir sonucu olan bir maliyet vardır. Ama genel olarak hayır .


10
+1 - harika yanıt! Şahsen ben bazen hayır yerine bazen ile bitirirdim . Yine de, bu sadece kişilik - sorunları çok iyi açıkladınız.
Rex Kerr

6
Değişmez değerler kullanan uygun bir uygulamanın zorunlu sürümlerin aksine hemen paralelleştirilebilir olduğunu eklemelisiniz. Modern teknolojik bağlamda, bu giderek daha önemli hale geliyor.
Raphael

Yardım ne kadar kullanır qsort lesser ++ (x : qsort greater)?
Solomon Ucko

42

Fonksiyonel programlamanın bir mihenk taşı olarak bunda yanlış olan birçok şey var. Öne çıkan özellikler şunları içerir:

  • Kutulu / kutulu olmayan ilkelleri kullanıyorsunuz. İlkel nesneleri sarmalamanın ek yükünü test etmeye değil, değişmezliği test etmeye çalışıyorsunuz.
  • Yerinde işlemin alışılmadık derecede etkili olduğu (ve kanıtlanabilir şekilde öyle olduğu) bir algoritma seçtiniz. Değişken bir şekilde uygulandığında daha hızlı olan algoritmaların var olduğunu göstermek istiyorsanız, bu iyi bir seçimdir; aksi takdirde, bu muhtemelen yanıltıcı olacaktır.
  • Yanlış zamanlama işlevini kullanıyorsunuz. Kullanın System.nanoTime.
  • Ölçüt, JIT derlemesinin ölçülen sürenin önemli bir parçası olmayacağından emin olmak için çok kısa.
  • Dizi verimli bir şekilde bölünmemiş.
  • Diziler değişebilir, bu yüzden onları FP ile kullanmak yine de garip bir karşılaştırma.

Dolayısıyla bu karşılaştırma, yüksek performanslı kod yazmak için dilinizi (ve algoritmanızı) ayrıntılı olarak anlamanız gereken harika bir örnektir. Ancak FP ile FP olmayanların karşılaştırması çok iyi değildir. Bunu istiyorsanız , Bilgisayar Dilleri Benchmark Game'de Haskell vs C ++ 'a bakın . Buradaki eve dönüş mesajı, cezanın tipik olarak 2 veya 3 faktörden fazla olmadığı, ancak gerçekten bağlı olduğu şeklindedir. (Haskell ailesinin mümkün olan en hızlı algoritmaları yazdığına dair sözler de yok, ama en azından bazıları muhtemelen denedi! Sonra yine, Haskell'in bazıları C kütüphanelerini çağırıyor ...)

Şimdi, daha makul bir Quicksort karşılaştırması istediğinizi varsayalım, bunun FP'ye karşı değiştirilebilir algoritmalar için muhtemelen en kötü durumlardan biri olduğunu kabul edin ve veri yapısı sorununu görmezden gelin (yani, değişmez bir Diziye sahip olabileceğimizi varsaymak):

object QSortExample {
  // Imperative mutable quicksort
  def swap(xs: Array[String])(a: Int, b: Int) {
    val t = xs(a); xs(a) = xs(b); xs(b) = t
  }
  def muQSort(xs: Array[String])(l: Int = 0, r: Int = xs.length-1) {
    val pivot = xs((l+r)/2)
    var a = l
    var b = r
    while (a <= b) {
      while (xs(a) < pivot) a += 1
      while (xs(b) > pivot) b -= 1
      if (a <= b) {
        swap(xs)(a,b)
        a += 1
        b -= 1
      }
    }
    if (l<b) muQSort(xs)(l, b)
    if (b<r) muQSort(xs)(a, r)
  }

  // Functional quicksort
  def fpSort(xs: Array[String]): Array[String] = {
    if (xs.length <= 1) xs
    else {
      val pivot = xs(xs.length/2)
      val (small,big) = xs.partition(_ < pivot)
      if (small.length == 0) {
        val (bigger,same) = big.partition(_ > pivot)
        same ++ fpSort(bigger)
      }
      else fpSort(small) ++ fpSort(big)
    }
  }

  // Utility function to repeat something n times
  def repeat[A](n: Int, f: => A): A = {
    if (n <= 1) f else { f; repeat(n-1,f) }
  }

  // This runs the benchmark
  def bench(n: Int, xs: Array[String], silent: Boolean = false) {
    // Utility to report how long something took
    def ptime[A](f: => A) = {
      val t0 = System.nanoTime
      val ans = f
      if (!silent) printf("elapsed: %.3f sec\n",(System.nanoTime-t0)*1e-9)
      ans
    }

    if (!silent) print("Scala builtin ")
    ptime { repeat(n, {
      val ys = xs.clone
      ys.sorted
    }) }
    if (!silent) print("Mutable ")
    ptime { repeat(n, {
      val ys = xs.clone
      muQSort(ys)()
      ys
    }) }
    if (!silent) print("Immutable ")
    ptime { repeat(n, {
      fpSort(xs)
    }) }
  }

  def main(args: Array[String]) {
    val letters = (1 to 500000).map(_ => scala.util.Random.nextPrintableChar)
    val unsorted = letters.grouped(5).map(_.mkString).toList.toArray

    repeat(3,bench(1,unsorted,silent=true))  // Warmup
    repeat(3,bench(10,unsorted))     // Actual benchmark
  }
}

İşlevsel Quicksort'taki değişikliği not edin, böylece veriler mümkünse yalnızca bir kez geçer ve yerleşik sıralama ile karşılaştırılır. Çalıştırdığımızda şöyle bir şey elde ederiz:

Scala builtin elapsed: 0.349 sec
Mutable elapsed: 0.445 sec
Immutable elapsed: 1.373 sec
Scala builtin elapsed: 0.343 sec
Mutable elapsed: 0.441 sec
Immutable elapsed: 1.374 sec
Scala builtin elapsed: 0.343 sec
Mutable elapsed: 0.442 sec
Immutable elapsed: 1.383 sec

Dolayısıyla, kendi türünüzü yazmaya çalışmanın kötü bir fikir olduğunu öğrenmenin yanı sıra, eğer ikincisi biraz dikkatli bir şekilde uygulanırsa, değişmez bir hızlı sıralama için ~ 3x ceza olduğunu görürüz. (Üç dizi döndüren bir trisect yöntemi de yazabilirsiniz: şundan küçük olanlar, eşit olanlar ve pivottan büyük olanlar. Bu, işleri biraz daha hızlandırabilir.)


Sadece boks / kutudan çıkarma ile ilgili. Bir şey varsa, bu java tarafında bir ceza olmalıdır, değil mi? Isnt Int, Scala için tercih edilen sayı türü (vs Tamsayı). Yani, ölçek tarafında boks olmuyor. Boks, yalnızca java tarafında bir sorundur çünkü otomatik kutulama, java.lang.Integer / int için scala Int oluşturur. işte bu konu hakkında ayrıntılı olarak bahseden
smartnut007

Evet, burada şeytanın avukatlığını oynuyorum. Değişkenlik, hızlı bağlantı tasarımının ayrılmaz bir parçasıdır. Bu yüzden soruna saf işlevsel yaklaşımı çok merak ettim. Ah, bu ifadeyi iplikte 10. kez söyledim :-). Uyandığımda ve geri döndüğümde gönderinin geri kalanına bakacağım. Teşekkürler.
smartnut007

2
@ smartnut007 - Scala koleksiyonları geneldir. Jenerikler, çoğunlukla kutulu türler gerektirir (ancak belirli ilkel türler için onları özelleştirme çabası devam etmektedir). Bu nedenle, tüm şık koleksiyon yöntemlerini kullanamazsınız ve ilkel türlerin koleksiyonlarını bunlardan geçirdiğinizde hiçbir ceza olmayacağını varsayamazsınız. İlkel türün içeri girerken kutulanması ve çıkarken kutusundan çıkarılması oldukça muhtemeldir.
Rex Kerr


1
@ smartnut007 - Bu büyük bir kusur çünkü kontrol etmesi zor ve eğer doğruysa sonuçları gerçekten mahvediyor. Boks olmadığından eminseniz, kusurun geçerli olmadığını kabul ediyorum. Kusur değil mi olduğunu size işte, boks bilmiyorum boks olup olmadığını (ve emin de değilim - uzmanlaşma anlamaya bu zor yapmıştır). Java tarafında (veya Scala değiştirilebilir uygulamasında) kutulama yoktur çünkü yalnızca ilkelleri kullanırsınız. Her neyse, değişmez bir sürüm n log n alan üzerinden çalışır, bu yüzden gerçekten karşılaştırma / takas maliyetini bellek ayırma ile karşılaştırırsınız.
Rex Kerr

10

Kullandığınız için Scala sürümünün aslında kuyruk özyinelemeli olduğunu düşünmüyorum Array.concat.

Ayrıca, bu deyimsel Scala kodu olduğu için, bu onu yapmanın en iyi yolu olduğu anlamına gelmez.

Bunu yapmanın en iyi yolu, Scala'nın yerleşik sıralama işlevlerinden birini kullanmaktır. Bu şekilde değişmezlik garantisi alırsınız ve hızlı bir algoritmanız olduğunu bilirsiniz.

Yığın Taşması sorusuna bakın Scala'da bir diziyi nasıl sıralayabilirim? örnek olarak.


4
ayrıca, iki özyinelemeli arama yapmanız gerektiğinden, kuyruk özyinelemeli hızlı sıralamanın mümkün olduğunu düşünmüyorum
Alex Lo

1
Mümkündür, yalnızca olası yığın çerçevelerinizi yığının üzerine kaldırmak için devam eden kapanışları kullanmanız gerekir.
Brian

dahili scala.util.Sorting.quickSort (dizi) diziyi değiştirir. Şaşırtıcı olmayan bir şekilde, java kadar hızlı çalışıyor. Etkili ve tamamen işlevsel bir çözümle ilgileniyorum. Değilse neden. Scala'nın bir sınırlaması mı yoksa genel olarak işlevsel paradigma mı? bu sorta şey.
smartnut007

@ smartnut007: Scala'nın hangi sürümünü kullanıyorsunuz? Scala 2.8'de, array.sortedyeni sıralanmış bir dizi döndüren, orijinal diziyi değiştirmeyen yapabilirsiniz.
missingfaktor

@AlexLo - bir kuyruk yinelemeli hızlı sıralama mümkündür. TAIL-RECURSIVE-QUICKSORT(Array A, int lo, int hi): while p < r: q = PARTITION(A, lo, hi); TAIL-RECURSIVE-QUICKSORT(A, lo, q - 1); p = q + 1;
Şunun

8

Değişmezlik pahalı değildir. Bir programın yapması gereken görevlerin küçük bir alt kümesini ölçüp, hızlı sıralamayı ölçmek gibi, önyükleme için değişkenliğe dayalı bir çözüm seçerseniz, kesinlikle pahalı olabilir.

Basitçe söylemek gerekirse, tamamen işlevsel dilleri kullanırken hızlı sıralama yapmazsınız.

Bunu başka bir açıdan ele alalım. Bu iki işlevi ele alalım:

// Version using mutable data structures
def tailFrom[T : ClassManifest](arr: Array[T], p: T => Boolean): Array[T] = {
  def posIndex(i: Int): Int = {
    if (i < arr.length) {
      if (p(arr(i)))
        i
      else
        posIndex(i + 1)
    } else {
      -1
    }
  }

  var index = posIndex(0)

  if (index < 0) Array.empty
  else {
    var result = new Array[T](arr.length - index)
    Array.copy(arr, index, result, 0, arr.length - index)
    result
  }
}

// Immutable data structure:

def tailFrom[T](list: List[T], p: T => Boolean): List[T] = {
  def recurse(sublist: List[T]): List[T] = {
    if (sublist.isEmpty) sublist
    else if (p(sublist.head)) sublist
    else recurse(sublist.tail)
  }
  recurse(list)
}

Bunu kıyaslayın ve değişken veri yapılarını kullanan kodun çok daha kötü performansa sahip olduğunu göreceksiniz, çünkü diziyi kopyalamaya ihtiyaç duyarken, değişmez kodun bununla ilgilenmesi gerekmez.

Değişmez veri yapıları ile program yaptığınızda, kodunuzu güçlü yönlerinden yararlanacak şekilde yapılandırırsınız. Bu sadece veri türü veya hatta bireysel algoritmalar değildir. Program farklı bir şekilde tasarlanacaktır .

Bu nedenle kıyaslama genellikle anlamsızdır. Ya bir stile ya da diğerine doğal olan algoritmaları seçersiniz ve bu stil kazanır ya da genellikle pratik olmayan tüm uygulamayı karşılaştırırsınız.


7

Bir diziyi sıralamak, evrendeki en zorunlu görev gibidir. Pek çok zarif 'değişmez' stratejinin / uygulamasının bir 'diziyi sırala' mikro ölçütünde başarısız olması şaşırtıcı değildir. Ancak bu, değişmezliğin "genel olarak" pahalı olduğu anlamına gelmez. Değişmez uygulamaların değiştirilebilir olanlarla karşılaştırılabilir şekilde çalışacağı birçok görev vardır, ancak dizi sıralaması genellikle bunlardan biri değildir.


7

Zorunlu algoritmalarınızı ve veri yapılarınızı işlevsel bir dile yeniden yazıyorsanız, bu gerçekten pahalı ve faydasız olacaktır. Her şeyi parlatmak için, yalnızca işlevsel programlamada bulunan özellikleri kullanmalısınız: veri yapılarının kalıcılığı, tembel değerlendirmeler vb.


Scala'da bir uygulama sağlamak için yeterince nazik misiniz?
smartnut007

3
powells.com/biblio/17-0521631246-0 (Tamamen Fonksiyonel Veri Yapıları, Chris Okasaki) - sadece bu kitaba bir göz atın. Etkili algoritmalar ve veri yapıları uygularken işlevsel programlama avantajlarından yararlanma konusunda anlatılacak güçlü bir hikayesi vardır.
Vasil Remeniuk

1
code.google.com/p/pfds Scala'da Debashish Ghosh tarafından uygulanan bazı veri yapıları
Vasil Remeniuk

Scala'nın neden zorunlu olmadığını düşündüğünüzü açıklayabilir misiniz? list.filter (foo).sort (bar).take (10)- daha zorunlu ne olabilir?
kullanıcı bilinmiyor

7

Scala'da değişmezliğin maliyeti

İşte neredeyse Java'dan daha hızlı olan bir sürüm. ;)

object QuickSortS {
  def sort(xs: Array[Int]): Array[Int] = {
    val res = new Array[Int](xs.size)
    xs.copyToArray(res)
    (new QuickSortJ).sort(res)
    res
  }
}

Bu sürüm dizinin bir kopyasını oluşturur, Java sürümünü kullanarak yerinde sıralar ve kopyayı döndürür. Scala sizi dahili olarak değişmez yapıyı kullanmaya zorlamaz.

Dolayısıyla Scala'nın faydası, uygun gördüğünüz şekilde değişkenlik ve değişmezlikten yararlanabilmenizdir. Dezavantajı ise, eğer bu yanlışı yaparsanız, değişmezliğin faydalarını gerçekten elde edemezsiniz.


Bu soruya kesin bir cevap olmasa da, bunun herhangi bir iyi cevabın parçası olduğunu düşünüyorum: Quicksort, değiştirilebilir bir yapı kullanıldığında daha hızlıdır. Ancak değişmezliğin başlıca avantajı arayüzdür ve Scala'da en azından ikisine birden sahip olabilirsiniz. Değişkenlik, hızlı sıralama için daha hızlıdır, ancak bu, yüksek performanslı, çoğunlukla değişmez kod yazma yolunuzda durmaz.
Paul Draper

7

QuickSort'un yerinde yapıldığında daha hızlı olduğu biliniyor, bu yüzden bu pek adil bir karşılaştırma değil!

Bunu söyledikten sonra ... Array.concat? Hiçbir şey değilse, zorunlu programlama için optimize edilmiş bir koleksiyon türünün, onu işlevsel bir algoritmada kullanmayı denediğinizde ve kullandığınızda özellikle yavaş olduğunu gösteriyorsunuz; hemen hemen her seçenek daha hızlı olacaktır!


Bir başka çok önemli bir nokta, belki de dikkate almak ikisini karşılaştırarak olduğunu yaklaştığında en önemli sorunu: "Birden düğümleri / çekirdek dışarı bu ölçek yapar ne kadar iyi"

Muhtemelen, değişmez bir hızlı sıralama arıyorsanız, bunu yapıyorsunuz çünkü aslında paralel bir hızlı sıralama istiyorsunuz. Wikipedia'da bu konuda bazı alıntılar var: http://en.wikipedia.org/wiki/Quicksort#Parallelizations

Scala sürümü, işlev tekrarlanmadan önce basitçe çatallanabilir ve böylece yeterli çekirdeğiniz varsa milyarlarca giriş içeren bir listeyi çok hızlı bir şekilde sıralayabilir.

Şu anda, sistemimdeki GPU, üzerinde Scala kodunu çalıştırabilirsem kullanabileceğim 128 çekirdeğe sahip ve bu, mevcut neslin iki yıl gerisinde olan basit bir masaüstü sisteminde.

Merak ettiğim tek iş parçacıklı zorunlu yaklaşıma karşı bu nasıl birikirdi ...

Belki de daha önemli olan soru şudur:

"Çekirdeklerin daha hızlı ilerlemeyeceği ve senkronizasyon / kilitlemenin paralelleştirme için gerçek bir zorluk teşkil ettiği göz önüne alındığında, değişkenlik pahalı mı?"


Orada tartışma yok. Hızlı sıralama, tanımı gereği bellek içi sıralamadır. Eminim çoğu insan bunu üniversiteden hatırlar. Ancak, saf işlevsel bir şekilde nasıl hızlı sıralama yaparsınız? yani yan etkiler olmadan.
smartnut007

Bunun önemli nedeni, işlevsel paradigmanın yan etkileri olan işlevler kadar hızlı olabileceği iddiaları vardır.
smartnut007

Liste sürümü, süreyi yarı yarıya azaltır. Yine de, java sürümünün hızına yakın bir yerde değil.
smartnut007

Scala'nın neden zorunlu olmadığını düşündüğünüzü açıklayabilir misiniz? list.filter (foo).sort (bar).take (10)- daha zorunlu ne olabilir? Teşekkürler.
kullanıcı bilinmiyor

@user bilinmiyor: Belki de "zorunlu" nun ne anlama geldiğini düşündüğünüzü netleştirebilirsiniz, çünkü belirttiğiniz örnek bana hoş bir şekilde işlevsel görünüyor. Scala'nın kendisi ne zorunlu ne de açıklayıcıdır, dil her iki stili de destekler ve bu terimler en iyi şekilde belirli örnekleri tanımlamak için kullanılır.
Kevin Wright

2

OO programlamanın karmaşıklığı gizlemek için soyutlamayı kullandığı ve işlevsel programlamanın karmaşıklığı ortadan kaldırmak için değişmezliği kullandığı söyleniyor. Scala'nın hibrit dünyasında, zorunlu kodu gizlemek için OO'yu kullanabiliriz ve uygulama kodunu daha akıllıca bırakmayabiliriz. Aslında koleksiyon kitaplıkları çok sayıda zorunlu kod kullanır, ancak bu onları kullanmamamız gerektiği anlamına gelmez. Başkalarının da dediği gibi, dikkatle kullanıldığında, burada gerçekten her iki dünyanın da en iyisini elde edersiniz.


Scala'nın neden zorunlu olmadığını düşündüğünüzü açıklayabilir misiniz? list.filter (foo).sort (bar).take (10)- daha zorunlu ne olabilir? Teşekkürler.
kullanıcı bilinmiyor

Scala'nın zorunlu olmadığını söylediğini nerede anlamıyorum.
Janx
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.