Hız ve optimizasyon üzerinde çalışırken, çok yanlış sonuçlar elde etmek çok kolaydır . Özellikle, karşılaştırma sürümünün derleyici sürümünden ve optimizasyon modundan bahsetmeden bir varyantın diğerinden daha hızlı olduğunu söyleyemezsiniz. O zaman bile, modern işlemciler, her türlü önbellekten bahsetmemek için sinir ağına dayalı dal tahmin edicilerine sahip olacak kadar karmaşıktır, bu nedenle dikkatli bir kurulumla bile kıyaslama sonuçları bulanık olacaktır.
Söyleniyor ki...
Kıyaslama bizim dostumuzdur.
criterion
gelişmiş karşılaştırma araçları sağlayan bir pakettir. Hızla böyle bir kriter hazırladım:
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 ]
Gördüğünüz gibi, bir kerede iki öğe üzerinde açıkça eşleşen varyantı ekledim, ancak aksi takdirde aynı kod kelimesi kelimesidir. Ben de önbellek nedeniyle önyargı farkında olmak için, ben de, kıstasları ters çalıştırmak. O zaman koşalım ve görelim!
% 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)
Bizim gibi görünüyor "yavaş" versiyonumuz hiç yavaş değil! Ve desen eşleştirmenin incelikleri hiçbir şey eklemez. ( match2
Art arda iki I koşusu arasında hafif bir hızlanma önbelleğe almanın etkilerine atfederim.)
Daha fazlasını elde etmenin bir yolu var "bilimsel" veri : -ddump-simpl
derleyicinin kodumuzu nasıl gördüğüne bakabiliriz.
Ara yapıların incelenmesi bizim dostumuzdur.
"Çekirdek" GHC'nin dahili bir dilidir. Her Haskell kaynak dosyası, çalışma zamanı sisteminin çalışması için son işlevsel grafiğe dönüştürülmeden önce Core'a sadeleştirilir. Bu ara aşamaya bakarsak, bize şunu söyleyecektir:myButLast
ve butLast2
eşdeğerdir. Yeniden adlandırılması aşamasında, tüm güzel tanımlayıcılarımız rastgele karıştırılır.
% 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)
Öyle görünüyor A1
ve A4
en benzerleri. Kapsamlı denetim, kod içeriğinin A1
veA4
aynı olduğunu aynı . Her ikisi de iki fonksiyonun bir bileşimi olarak tanımlandığından, A2
ve A3
benzerleri de mantıklıdır.
core
Çıktıyı kapsamlı bir şekilde inceleyecekseniz, bayrak tedarik etmek de mantıklıdır-dsuppress-module-prefixes
ve gibi-dsuppress-uniques
. Okumayı çok kolaylaştırırlar.
Düşmanlarımızın da kısa bir listesi.
Öyleyse, kıyaslama ve optimizasyon ile ilgili sorun ne olabilir?
ghci
, etkileşimli oyun ve hızlı yineleme için tasarlanan Haskell kaynağını, son yürütülebilir dosyadan ziyade belirli bir bayt kodu lezzetiyle derler ve daha hızlı yeniden yükleme lehine pahalı optimizasyonlardan kaçınır.
- Profil oluşturma, tek tek bitlerin ve karmaşık bir programın parçalarının performansını incelemek için güzel bir araç gibi görünüyor, ancak derleyici optimizasyonlarını çok kötü bir şekilde bozabilir, sonuçlar tabandan büyüklük emirleri olacaktır.
- Güvenliğiniz, her küçük kod parçasını ayrı bir yürütülebilir dosya olarak ve kendi karşılaştırma ölçütü ile profillemektir.
- Çöp toplama ayarlanabilir. Sadece bugün yeni bir büyük özellik yayınlandı.Çöp toplama gecikmeleri performansı tahmin etmesi kolay olmayan şekillerde etkiler.
- Bahsettiğim gibi, farklı derleyici sürümleri farklı performansa sahip farklı kodlar oluşturacaktır, bu nedenle herhangi bir söz vermeden önce kodunuzun kullanıcısının bu sürümü oluşturmak için hangi sürümü kullanacağını bilmeniz gerekir.
Bu üzücü görünebilir. Ancak çoğu zaman bir Haskell programcısıyla ilgili olması gereken şey bu değildir. Gerçek hikaye: Yakın zamanda Haskell öğrenmeye başlayan bir arkadaşım var. Sayısal entegrasyon için bir program yazmışlardı ve kaplumbağa yavaştı. Bu yüzden birlikte oturduk ve algoritmanın diyagramlar ve şeyler ile kategorik bir açıklaması yazdık . Kodu soyut tanımla hizalamak için yeniden yazdıklarında, sihirli bir şekilde çita hızlı ve hafızada zayıfladı. Kısa sürede π hesapladık. Hikayeden çıkarılacak ders? Mükemmel soyut yapı ve kodunuz kendini optimize edecektir.
init
listenin birden çok kez "paketten çıkarılmasını" önlemek için optimize edilmiştir.