GHC tidak mengotori fungsi.
Namun, ia menghitung setiap ekspresi yang diberikan dalam kode paling banyak sekali setiap kali ekspresi lambda di sekitarnya dimasukkan, atau paling banyak sekali jika berada di level teratas. Menentukan di mana ekspresi lambda bisa menjadi sedikit rumit saat Anda menggunakan gula sintaksis seperti dalam contoh Anda, jadi mari kita ubah ini menjadi sintaks desugared yang setara:
m1' = (!!) (filter odd [1..]) -- NB: See below!
m2' = \n -> (!!) (filter odd [1..]) n
(Catatan: Laporan Haskell 98 sebenarnya menjelaskan bagian operator kiri seperti yang (a %)
setara dengan \b -> (%) a b
, tetapi GHC mendeskripsikannya (%) a
. Ini secara teknis berbeda karena dapat dibedakan oleh seq
. Saya rasa saya mungkin telah mengirimkan tiket GHC Trac tentang hal ini.)
Diberikan ini, Anda dapat melihat bahwa di m1'
, ekspresi filter odd [1..]
tidak terdapat dalam ekspresi lambda, jadi itu hanya akan dihitung sekali per jalannya program Anda, sementara di m2'
, filter odd [1..]
akan dihitung setiap kali ekspresi lambda dimasukkan, yaitu, pada setiap panggilan m2'
. Itu menjelaskan perbedaan waktu yang Anda lihat.
Sebenarnya, beberapa versi GHC, dengan opsi pengoptimalan tertentu, akan berbagi lebih banyak nilai daripada yang ditunjukkan oleh uraian di atas. Ini bisa menjadi masalah dalam beberapa situasi. Misalnya, perhatikan fungsinya
f = \x -> let y = [1..30000000] in foldl' (+) 0 (y ++ [x])
GHC mungkin memperhatikan bahwa y
tidak bergantung pada x
dan menulis ulang fungsi ke
f = let y = [1..30000000] in \x -> foldl' (+) 0 (y ++ [x])
Dalam hal ini, versi baru jauh kurang efisien karena harus membaca sekitar 1 GB dari memori tempat y
disimpan, sedangkan versi asli akan berjalan dalam ruang konstan dan masuk ke dalam cache prosesor. Faktanya, di bawah GHC 6.12.1, fungsinya f
hampir dua kali lebih cepat saat dikompilasi tanpa pengoptimalan daripada saat dikompilasi -O2
.
seq
m1 10000000). Namun ada perbedaan ketika tidak ada tanda pengoptimalan yang ditentukan. Dan kedua varian "f" Anda memiliki residensi maksimum 5356 byte terlepas dari pengoptimalannya (dengan alokasi total yang lebih sedikit ketika -O2 digunakan).f
:main = interact $ unlines . (show . map f . read) . lines
; kompilasi dengan atau tanpa-O2
; laluecho 1 | ./main
. Jika Anda menulis tes sepertimain = print (f 5)
, makay
sampah dapat dikumpulkan seperti yang digunakan dan tidak ada perbedaan antara keduanyaf
.map (show . f . read)
, tentu saja. Dan sekarang saya telah mengunduh GHC 6.12.3, saya melihat hasil yang sama seperti di GHC 6.12.1. Dan ya, Anda benar tentang yang aslim1
danm2
: versi GHC yang melakukan pengangkatan semacam ini dengan pengoptimalan diaktifkan akan berubahm2
menjadim1
.m1 hanya dihitung sekali karena merupakan Formulir Aplikasi Konstan, sedangkan m2 bukan CAF, dan dihitung untuk setiap evaluasi.
Lihat wiki GHC di CAFs: http://www.haskell.org/haskellwiki/Constant_applicative_form
sumber
[1 ..]
tersebut dihitung hanya sekali selama eksekusi program atau dihitung sekali per aplikasi fungsi, tetapi apakah ini terkait dengan CAF?m1
CAF, yang kedua berlaku danfilter odd [1..]
(bukan hanya[1..]
!) Dihitung hanya sekali. GHC juga dapat mencatat yangm2
mengacu padafilter odd [1..]
, dan menempatkan tautan ke thunk yang sama yang digunakanm1
, tetapi itu akan menjadi ide yang buruk: ini dapat menyebabkan kebocoran memori yang besar dalam beberapa situasi.[1..]
danfilter odd [1..]
. Selebihnya, saya masih belum yakin. Jika saya tidak salah, CAF hanya relevan ketika kita ingin menyatakan bahwa kompiler dapat menggantikanfilter odd [1..]
inm2
oleh global thunk (yang bahkan mungkin sama dengan yang digunakan dim1
). Tetapi dalam situasi penanya, kompilator tidak melakukan "pengoptimalan" itu, dan saya tidak dapat melihat relevansinya dengan pertanyaan.m1
, dan itu tidak.Ada perbedaan penting antara kedua bentuk tersebut: batasan monomorfisme berlaku untuk m1 tetapi tidak untuk m2, karena m2 secara eksplisit memberikan argumen. Jadi tipe m2 adalah umum tetapi m1 spesifik. Jenis yang ditetapkan adalah:
Kebanyakan kompiler dan interpreter Haskell (semuanya yang saya tahu sebenarnya) tidak mengotori struktur polimorfik, jadi daftar internal m2 dibuat ulang setiap kali dipanggil, di mana m1 tidak.
sumber
Saya tidak yakin, karena saya sendiri masih baru mengenal Haskell, tetapi tampaknya ini karena fungsi kedua adalah parametrized dan yang pertama tidak. Sifat dari fungsinya adalah, hasil tergantung pada nilai input dan dalam paradigma fungsional terutama hanya bergantung pada input. Implikasi yang jelas adalah bahwa fungsi tanpa parameter selalu mengembalikan nilai yang sama berulang kali, apa pun yang terjadi.
Ternyata ada mekanisme pengoptimalan dalam kompilator GHC yang memanfaatkan fakta ini untuk menghitung nilai fungsi seperti itu hanya sekali untuk keseluruhan waktu proses program. Ia melakukannya dengan malas, untuk memastikannya, tapi tetap melakukannya. Saya memperhatikannya sendiri, ketika saya menulis fungsi berikut:
Kemudian untuk mengujinya, saya masuk GHCI dan menulis:
primes !! 1000
. Butuh beberapa detik, tapi akhirnya saya mendapat jawaban:7927
. Lalu aku meneleponprimes !! 1001
dan mendapat jawabannya seketika. Demikian pula dalam sekejap saya mendapatkan hasil untuktake 1000 primes
, karena Haskell harus menghitung seluruh daftar seribu elemen untuk mengembalikan elemen ke-1001 sebelumnya.Jadi, jika Anda dapat menulis fungsi Anda sedemikian rupa sehingga tidak membutuhkan parameter, Anda mungkin menginginkannya. ;)
sumber