Saat mempelajari kecepatan dan pengoptimalan, sangat mudah untuk mendapatkan hasil yang sangat salah . Secara khusus, Anda tidak dapat benar-benar mengatakan bahwa satu varian lebih cepat daripada yang lain tanpa menyebutkan versi kompiler dan mode pengoptimalan pengaturan pembandingan Anda. Bahkan kemudian, prosesor modern sangat canggih untuk fitur prediktor cabang berbasis jaringan saraf, belum lagi semua jenis cache, sehingga, bahkan dengan pengaturan yang hati-hati, hasil pembandingan akan buram.
Yang telah dibilang...
Benchmarking adalah teman kita.
criterion
adalah paket yang menyediakan alat pembandingan lanjutan. Saya dengan cepat membuat tolok ukur seperti ini:
module Main where
import Criterion
import Criterion.Main
-- slow
myButLast :: [a] -> a
myButLast [x, y] = x
myButLast (x : xs) = myButLast xs
myButLast _ = error "List too short"
-- decent
myButLast' :: [a] -> a
myButLast' = (!! 1) . reverse
-- fast
myButLast'' :: [a] -> a
myButLast'' = last . init
butLast2 :: [a] -> a
butLast2 (x : _ : [ ] ) = x
butLast2 (_ : xs@(_ : _ ) ) = butLast2 xs
butLast2 _ = error "List too short"
setupEnv = do
let xs = [1 .. 10^7] :: [Int]
return xs
benches xs =
[ bench "slow?" $ nf myButLast xs
, bench "decent?" $ nf myButLast' xs
, bench "fast?" $ nf myButLast'' xs
, bench "match2" $ nf butLast2 xs
]
main = defaultMain
[ env setupEnv $ \ xs -> bgroup "main" $ let bs = benches xs in bs ++ reverse bs ]
Seperti yang Anda lihat, saya menambahkan varian yang secara eksplisit cocok dengan dua elemen sekaligus, tetapi sebaliknya itu adalah kode yang sama kata demi kata. Saya juga menjalankan tolok ukur secara terbalik, agar menyadari bias karena caching. Jadi, mari kita lari dan lihat!
% ghc --version
The Glorious Glasgow Haskell Compilation System, version 8.6.5
% ghc -O2 -package criterion A.hs && ./A
benchmarking main/slow?
time 54.83 ms (54.75 ms .. 54.90 ms)
1.000 R² (1.000 R² .. 1.000 R²)
mean 54.86 ms (54.82 ms .. 54.93 ms)
std dev 94.77 μs (54.95 μs .. 146.6 μs)
benchmarking main/decent?
time 794.3 ms (32.56 ms .. 1.293 s)
0.907 R² (0.689 R² .. 1.000 R²)
mean 617.2 ms (422.7 ms .. 744.8 ms)
std dev 201.3 ms (105.5 ms .. 283.3 ms)
variance introduced by outliers: 73% (severely inflated)
benchmarking main/fast?
time 84.60 ms (84.37 ms .. 84.95 ms)
1.000 R² (1.000 R² .. 1.000 R²)
mean 84.46 ms (84.25 ms .. 84.77 ms)
std dev 435.1 μs (239.0 μs .. 681.4 μs)
benchmarking main/match2
time 54.87 ms (54.81 ms .. 54.95 ms)
1.000 R² (1.000 R² .. 1.000 R²)
mean 54.85 ms (54.81 ms .. 54.92 ms)
std dev 104.9 μs (57.03 μs .. 178.7 μs)
benchmarking main/match2
time 50.60 ms (47.17 ms .. 53.01 ms)
0.993 R² (0.981 R² .. 0.999 R²)
mean 60.74 ms (56.57 ms .. 67.03 ms)
std dev 9.362 ms (6.074 ms .. 10.95 ms)
variance introduced by outliers: 56% (severely inflated)
benchmarking main/fast?
time 69.38 ms (56.64 ms .. 78.73 ms)
0.948 R² (0.835 R² .. 0.994 R²)
mean 108.2 ms (92.40 ms .. 129.5 ms)
std dev 30.75 ms (19.08 ms .. 37.64 ms)
variance introduced by outliers: 76% (severely inflated)
benchmarking main/decent?
time 770.8 ms (345.9 ms .. 1.004 s)
0.967 R² (0.894 R² .. 1.000 R²)
mean 593.4 ms (422.8 ms .. 691.4 ms)
std dev 167.0 ms (50.32 ms .. 226.1 ms)
variance introduced by outliers: 72% (severely inflated)
benchmarking main/slow?
time 54.87 ms (54.77 ms .. 55.00 ms)
1.000 R² (1.000 R² .. 1.000 R²)
mean 54.95 ms (54.88 ms .. 55.10 ms)
std dev 185.3 μs (54.54 μs .. 251.8 μs)
Sepertinya versi "lambat" kami sama sekali tidak lambat! Dan seluk-beluk pencocokan pola tidak menambahkan apa pun. (Sedikit mempercepat kita melihat antara dua kali berturut-turut match2
saya menganggap efek caching.)
Ada cara untuk mendapatkan lebih banyak data "ilmiah" : kita dapat -ddump-simpl
dan melihat cara kompiler melihat kode kita.
Inspeksi struktur menengah adalah teman kita.
"Core" adalah bahasa internal GHC. Setiap file sumber Haskell disederhanakan menjadi Core sebelum diubah menjadi grafik fungsional akhir untuk dijalankan oleh sistem waktu. Jika kita melihat tahap menengah ini, ia akan memberi tahu kita myButLast
dan butLast2
setara. Memang butuh melihat, karena, pada tahap penggantian nama, semua pengenal baik kami acak-acakan.
% for i in `seq 1 4`; do echo; cat A$i.hs; ghc -O2 -ddump-simpl A$i.hs > A$i.simpl; done
module A1 where
-- slow
myButLast :: [a] -> a
myButLast [x, y] = x
myButLast (x : xs) = myButLast xs
myButLast _ = error "List too short"
module A2 where
-- decent
myButLast' :: [a] -> a
myButLast' = (!! 1) . reverse
module A3 where
-- fast
myButLast'' :: [a] -> a
myButLast'' = last . init
module A4 where
butLast2 :: [a] -> a
butLast2 (x : _ : [ ] ) = x
butLast2 (_ : xs@(_ : _ ) ) = butLast2 xs
butLast2 _ = error "List too short"
% ./EditDistance.hs *.simpl
(("A1.simpl","A2.simpl"),3866)
(("A1.simpl","A3.simpl"),3794)
(("A2.simpl","A3.simpl"),663)
(("A1.simpl","A4.simpl"),607)
(("A2.simpl","A4.simpl"),4188)
(("A3.simpl","A4.simpl"),4113)
Tampaknya A1
dan A4
yang paling mirip. Pemeriksaan menyeluruh akan menunjukkan bahwa memang struktur kode A1
dan A4
identik. Itu A2
dan A3
sama juga masuk akal karena keduanya didefinisikan sebagai komposisi dua fungsi.
Jika Anda akan memeriksa core
output secara luas, masuk akal juga untuk menyediakan flag seperti -dsuppress-module-prefixes
dan -dsuppress-uniques
. Mereka membuatnya jauh lebih mudah dibaca.
Daftar pendek musuh kita juga.
Jadi, apa yang bisa salah dengan pembandingan dan pengoptimalan?
ghci
, yang dirancang untuk permainan interaktif dan iterasi cepat, mengkompilasi sumber Haskell ke rasa kode byte tertentu, daripada dieksekusi akhir, dan menghindari optimasi mahal demi pemuatan ulang yang lebih cepat.
- Profiling sepertinya alat yang bagus untuk melihat kinerja bit-bit individual dari sebuah program yang kompleks, tetapi itu dapat merusak optimisasi kompiler dengan sangat buruk, hasilnya akan menjadi urutan besarnya dari basis.
- Perlindungan Anda adalah membuat profil setiap bit kecil kode sebagai executable terpisah, dengan runner benchmark sendiri.
- Pengumpulan sampah bisa dijamah. Baru hari ini fitur utama baru dirilis. Penundaan pengumpulan sampah akan memengaruhi kinerja dengan cara yang tidak mudah diprediksi.
- Seperti yang saya sebutkan, versi kompiler yang berbeda akan membangun kode yang berbeda dengan kinerja yang berbeda, jadi Anda harus tahu versi apa yang akan digunakan oleh pengguna kode Anda untuk membuatnya, dan membandingkannya dengan itu, sebelum Anda membuat janji.
Ini mungkin terlihat sedih. Tetapi sebenarnya bukan hal yang harus menjadi perhatian seorang programmer Haskell, sebagian besar waktu. Kisah nyata: Saya punya teman yang baru saja mulai belajar Haskell. Mereka telah menulis sebuah program untuk integrasi numerik, dan itu lambat sekali. Jadi kami duduk bersama dan menulis deskripsi kategoris dari algoritma, dengan diagram dan sebagainya. Ketika mereka menulis ulang kode untuk menyelaraskan dengan deskripsi abstrak, secara ajaib menjadi, seperti, cheetah cepat, dan langsing pada memori juga. Kami menghitung π dalam waktu singkat. Pesan moral dalam cerita? Struktur abstrak yang sempurna, dan kode Anda akan mengoptimalkan dirinya sendiri.
init
telah dioptimalkan untuk menghindari "membongkar" daftar beberapa kali.