Bir listenin son fakat ikinci öğesini bulurken neden `` son '' bunlar arasında en hızlı olanı kullanıyorsunuz?


10

Bir listede son fakat ikinci elemanı bulan 3 fonksiyon bulunmaktadır. Kullanan last . initdiğerlerinden çok daha hızlı görünüyor. Nedenini anlayamıyorum.

Test için, [1..100000000](100 milyon) giriş listesi kullandım . Sonuncusu neredeyse anında çalışır, diğerleri ise birkaç saniye sürer.

-- 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

5
initlistenin birden çok kez "paketten çıkarılmasını" önlemek için optimize edilmiştir.
Willem Van Onsem

1
@WillemVanOnsem ama neden myButLastdaha yavaş? Görünüşe göre herhangi bir listeyi init
açmak

1
@Ismor: o [x, y]kısaltmasıdır (x:(y:[]))ikinci kuyruk ise dış olumsuz yönleri, ikinci bir olumsuz yönleri, ve kontroller açar, böylece, consbir []. Ayrıca ikinci fıkra listeyi tekrar açacaktır (x:xs). Evet, ambalajın açılması oldukça verimlidir, ancak elbette çok sık meydana gelirse, bu işlemi yavaşlatacaktır.
Willem Van Onsem

1
Hackage.haskell.org/package/base-4.12.0.0/docs/src/… 'a baktığımızda , optimizasyonun initargümanının tekil bir liste mi yoksa boş bir liste mi olduğunu tekrar tekrar kontrol etmediği görülüyor . Özyineleme başladığında, ilk öğenin özyinelemeli çağrının sonucuna bağlanacağını varsayar.
chepner

2
@WillemVanOnsem Paketten çıkarmanın muhtemelen burada sorun olmadığını düşünüyorum: GHC, myButLastotomatik olarak optimize edilmiş sürümünü vermesi gereken çağrı deseni uzmanlığı yapıyor . Ben hızlanma için suçlama daha büyük olasılıkla liste füzyon düşünüyorum.
oisdk

Yanıtlar:


9

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.

criteriongeliş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. ( match2Art 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-simplderleyicinin 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 butLast2eş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 A1ve A4en benzerleri. Kapsamlı denetim, kod içeriğinin A1veA4 aynı olduğunu aynı . Her ikisi de iki fonksiyonun bir bileşimi olarak tanımlandığından, A2ve A3benzerleri 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.


Çok bilgilendirici ve aynı zamanda bu aşamada benim için biraz ezici. Bu durumda, yaptığım tüm "kıyaslama" 100 milyon öğe listesi için tüm işlevi yürüttü ve birinin diğerinden daha uzun sürdüğünü fark etti. Ölçütlü kıyaslama oldukça yararlı görünüyor. Buna ek olarak, ghcisöylediğin gibi, önce bir exe yapmakla karşılaştırıldığında (hız açısından) farklı sonuçlar veriyor gibi görünüyor.
storm125
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.