Pertanyaannya ada dalam dua bagian. Yang pertama adalah konseptual. Berikutnya melihat pertanyaan yang sama secara lebih konkrit di Scala.
- Apakah hanya menggunakan struktur data yang tidak dapat diubah dalam bahasa pemrograman membuat penerapan algoritme / logika tertentu secara inheren lebih mahal secara komputasi dalam praktiknya? Ini menarik fakta bahwa keabadian adalah prinsip inti dari bahasa fungsional murni. Apakah ada faktor lain yang mempengaruhi hal ini?
- Mari kita ambil contoh yang lebih konkret. Quicksort umumnya diajarkan dan diimplementasikan menggunakan operasi yang dapat berubah pada struktur data dalam memori. Bagaimana seseorang mengimplementasikan hal seperti itu dengan cara fungsional PURE dengan overhead komputasi dan penyimpanan yang sebanding dengan versi yang bisa berubah. Khususnya di Scala. Saya telah menyertakan beberapa tolok ukur kasar di bawah ini.
Keterangan lebih lanjut:
Saya berasal dari latar belakang pemrograman imperatif (C ++, Java). Saya telah menjelajahi pemrograman fungsional, khususnya Scala.
Beberapa prinsip utama pemrograman fungsional murni:
- Fungsi adalah warga negara kelas satu.
- Fungsi tidak memiliki efek samping dan karenanya objek / struktur data tidak dapat diubah .
Meskipun JVM modern sangat efisien dengan pembuatan objek dan pengumpulan sampah sangat murah untuk objek berumur pendek, mungkin lebih baik meminimalkan pembuatan objek, bukan? Setidaknya dalam aplikasi single-threaded di mana konkurensi dan penguncian tidak menjadi masalah. Karena Scala adalah paradigma hybrid, seseorang dapat memilih untuk menulis kode imperatif dengan objek yang bisa berubah jika perlu. Tapi, sebagai seseorang yang telah menghabiskan banyak waktu mencoba menggunakan kembali objek dan meminimalkan alokasi. Saya ingin pemahaman yang baik tentang aliran pemikiran yang bahkan tidak mengizinkan hal itu.
Sebagai kasus khusus, saya sedikit terkejut dengan potongan kode ini dalam tutorial 6 ini . Ini memiliki Quicksort versi Java yang diikuti dengan implementasi Scala yang tampak rapi.
Berikut adalah upaya saya untuk mengukur implementasi. Saya belum melakukan profil mendetail. Tapi, tebakan saya adalah bahwa versi Scala lebih lambat karena jumlah objek yang dialokasikan adalah linier (satu per panggilan rekursi). Adakah kemungkinan bahwa pengoptimalan panggilan ekor bisa ikut bermain? Jika saya benar, Scala mendukung pengoptimalan panggilan ekor untuk panggilan rekursif sendiri. Jadi, seharusnya hanya membantu saja. Saya menggunakan Scala 2.8.
Versi Java
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;
}
}
Versi Scala
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 <)))
}
}
Kode Scala untuk membandingkan implementasi
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"))
Hasil
Waktu dalam milidetik untuk lima kali berturut-turut
Immutable/Functional/Scala 467 178 184 187 183
Mutable/Imperative/Java 51 14 12 12 12
O(n)
rangkaian daftar. Ini lebih pendek dari versi pseudocode;)