Kemalasan
Ini bukan "optimisasi kompiler", tetapi ini sesuatu yang dijamin oleh spesifikasi bahasa, sehingga Anda selalu dapat mengandalkannya. Pada dasarnya, ini berarti bahwa pekerjaan tidak dilakukan sampai Anda "melakukan sesuatu" dengan hasilnya. (Kecuali jika Anda melakukan salah satu dari beberapa hal untuk mematikan kemalasan dengan sengaja.)
Ini, jelas, adalah seluruh topik dalam dirinya sendiri, dan SO sudah punya banyak pertanyaan dan jawaban tentang hal itu.
Dalam pengalaman saya yang terbatas, membuat kode Anda terlalu malas atau terlalu ketat memiliki penalti kinerja yang jauh lebih besar (dalam waktu dan ruang) daripada hal-hal lain yang akan saya bicarakan ...
Analisis keketatan
Kemalasan adalah tentang menghindari pekerjaan kecuali jika itu perlu. Jika kompilator dapat menentukan bahwa hasil yang diberikan akan "selalu" diperlukan, maka tidak akan repot menyimpan perhitungan dan melakukannya nanti; itu hanya akan melakukannya secara langsung, karena itu lebih efisien. Ini disebut "analisis ketat".
Gotcha, jelas, adalah bahwa kompiler tidak selalu dapat mendeteksi kapan sesuatu dapat dibuat ketat. Terkadang Anda perlu memberi sedikit kompiler petunjuk. (Saya tidak mengetahui cara mudah untuk menentukan apakah analisis ketelitian telah melakukan apa yang Anda pikirkan, selain mengarungi keluaran Core.)
Sebaris
Jika Anda memanggil suatu fungsi, dan kompiler dapat mengetahui fungsi mana yang Anda panggil, ia mungkin mencoba untuk "inline" fungsi itu - yaitu, untuk mengganti panggilan fungsi dengan salinan fungsi itu sendiri. Overhead panggilan fungsi biasanya cukup kecil, tetapi inlining sering memungkinkan optimisasi lain terjadi yang tidak akan terjadi sebaliknya, sehingga inlining bisa menjadi kemenangan besar.
Fungsi hanya diuraikan jika "cukup kecil" (atau jika Anda menambahkan pragma yang secara khusus meminta inlining). Selain itu, fungsi hanya dapat digarisbawahi jika kompiler dapat mengetahui fungsi apa yang Anda panggil. Ada dua cara utama yang tidak bisa diketahui oleh kompiler:
Jika fungsi yang Anda panggil diteruskan dari tempat lain. Misalnya, ketika filter
fungsi dikompilasi, Anda tidak dapat menyamakan predikat filter, karena itu argumen yang disediakan pengguna.
Jika fungsi yang Anda panggil adalah metode kelas dan kompiler tidak tahu jenis apa yang terlibat. Misalnya, ketika sum
fungsi dikompilasi, kompiler tidak dapat menyejajarkan +
fungsi, karena sum
berfungsi dengan beberapa tipe angka yang berbeda, masing-masing memiliki +
fungsi yang berbeda .
Dalam kasus terakhir, Anda dapat menggunakan {-# SPECIALIZE #-}
pragma untuk menghasilkan versi fungsi yang dikodekan secara keras ke tipe tertentu. Misalnya, {-# SPECIALIZE sum :: [Int] -> Int #-}
akan mengkompilasi versi sum
hard-coded untuk Int
tipe tersebut, artinya +
dapat digarisbawahi dalam versi ini.
Namun, perlu diketahui bahwa sum
fungsi khusus baru kami hanya akan dipanggil ketika kompiler dapat mengetahui bahwa kami sedang bekerja dengannya Int
. Kalau tidak yang asli, polimorfik sum
akan dipanggil. Sekali lagi, overhead panggilan fungsi sebenarnya cukup kecil. Optimalisasi tambahan inilah yang memungkinkan inlining dapat memberikan manfaat.
Penghapusan subekspresi umum
Jika suatu blok kode tertentu menghitung nilai yang sama dua kali, kompilator dapat menggantikannya dengan satu instance dari perhitungan yang sama. Misalnya, jika Anda melakukannya
(sum xs + 1) / (sum xs + 2)
maka kompiler dapat mengoptimalkan ini untuk
let s = sum xs in (s+1)/(s+2)
Anda mungkin berharap bahwa kompiler akan selalu melakukan ini. Namun, ternyata dalam beberapa situasi ini dapat menghasilkan kinerja yang lebih buruk, tidak lebih baik, sehingga GHC tidak selalu melakukan ini. Terus terang, saya tidak begitu mengerti detail di balik yang ini. Tetapi intinya adalah, jika transformasi ini penting bagi Anda, tidak sulit untuk melakukannya secara manual. (Dan jika itu tidak penting, mengapa kamu mengkhawatirkannya?)
Ekspresi kasus
Pertimbangkan yang berikut ini:
foo (0:_ ) = "zero"
foo (1:_ ) = "one"
foo (_:xs) = foo xs
foo ( []) = "end"
Tiga persamaan pertama semua memeriksa apakah daftar tersebut tidak kosong (antara lain). Tetapi memeriksa hal yang sama tiga kali sia-sia. Untungnya, sangat mudah bagi kompiler untuk mengoptimalkan ini menjadi beberapa ekspresi kasus bersarang. Dalam hal ini, sesuatu seperti
foo xs =
case xs of
y:ys ->
case y of
0 -> "zero"
1 -> "one"
_ -> foo ys
[] -> "end"
Ini agak kurang intuitif, tetapi lebih efisien. Karena kompiler dapat dengan mudah melakukan transformasi ini, Anda tidak perlu khawatir tentang hal itu. Cukup tulis pencocokan pola Anda dengan cara yang paling intuitif; kompiler sangat pandai mengatur ulang dan mengatur ulang ini untuk membuatnya secepat mungkin.
Fusi
Idi standar Haskell untuk pemrosesan daftar adalah untuk menyatukan fungsi-fungsi yang mengambil satu daftar dan menghasilkan daftar baru. Contoh kanonik adalah
map g . map f
Sayangnya, sementara kemalasan menjamin melewatkan pekerjaan yang tidak perlu, semua alokasi dan alokasi untuk kinerja menengah daftar getah. "Fusion" atau "penggundulan hutan" adalah tempat penyusun mencoba menghilangkan langkah-langkah perantara ini.
Masalahnya adalah, sebagian besar fungsi ini bersifat rekursif. Tanpa rekursi, itu akan menjadi latihan dasar dalam inlining untuk memadatkan semua fungsi menjadi satu blok kode besar, menjalankan penyederhanaan di atasnya dan menghasilkan kode yang benar-benar optimal tanpa daftar perantara. Tetapi karena rekursi, itu tidak akan berhasil.
Anda dapat menggunakan {-# RULE #-}
pragma untuk memperbaikinya. Sebagai contoh,
{-# RULES "map/map" forall f g xs. map f (map g xs) = map (f.g) xs #-}
Sekarang setiap kali GHC melihat map
diterapkan map
, itu squishes menjadi satu pass di atas daftar, menghilangkan daftar perantara.
Masalahnya adalah, ini hanya berfungsi untuk map
diikuti oleh map
. Ada banyak kemungkinan lain - map
diikuti oleh filter
, filter
diikuti olehmap
, dll. Daripada menggunakan kode tangan solusi untuk masing-masing dari mereka, apa yang disebut "aliran fusi" diciptakan. Ini adalah trik yang lebih rumit, yang tidak akan saya uraikan di sini.
Panjang dan pendeknya adalah: Ini semua adalah trik optimasi khusus yang ditulis oleh programmer . GHC sendiri tidak tahu apa-apa tentang fusi; itu semua ada di daftar perpustakaan dan perpustakaan kontainer lainnya. Jadi optimasi apa yang terjadi tergantung pada bagaimana perpustakaan kontainer Anda ditulis (atau, lebih realistis, perpustakaan mana yang Anda pilih untuk digunakan).
Misalnya, jika Anda bekerja dengan array Haskell '98, jangan mengharapkan fusi dalam bentuk apa pun. Tetapi saya mengerti bahwa vector
perpustakaan memiliki kemampuan fusi yang luas. Ini semua tentang perpustakaan; kompiler hanya menyediakan RULES
pragma. (Omong-omong, ini sangat kuat. Sebagai penulis perpustakaan, Anda dapat menggunakannya untuk menulis ulang kode klien!)
Meta:
Saya setuju dengan orang-orang yang mengatakan "kode pertama, profil kedua, optimalkan ketiga".
Saya juga setuju dengan orang-orang yang mengatakan "akan bermanfaat untuk memiliki model mental untuk berapa banyak biaya keputusan desain yang diberikan".
Seimbangkan semua hal, dan semua itu ...