속도와 최적화를 연구 할 때 매우 잘못된 결과 를 얻는 것은 매우 쉽습니다 . 특히, 컴파일러 버전과 벤치마킹 설정의 최적화 모드를 언급하지 않고 한 변형이 다른 변형보다 빠르다고 말할 수는 없습니다. 그럼에도 불구하고 최신 프로세서는 신경망 기반 분기 예측 기능을 갖추고있어 모든 종류의 캐시를 언급 할 수 없을 정도로 정교하므로 신중하게 설정하더라도 벤치마킹 결과가 흐려질 수 있습니다.
그 말은 ...
벤치마킹은 우리의 친구입니다.
criterion고급 벤치마킹 도구를 제공하는 패키지입니다. 나는 다음과 같이 신속하게 벤치 마크를 작성했습니다.
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 ]
보시다시피, 한 번에 두 요소에서 명시 적으로 일치하는 변형을 추가했지만 그렇지 않으면 동일한 코드입니다. 또한 캐싱으로 인한 바이어스를 인식하기 위해 벤치 마크를 반대로 실행합니다. 그래서 우리가 달리고 보자!
% 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)
우리처럼 보인다 "느린" 버전은 전혀하지 느리다! 그리고 패턴 매칭의 복잡성은 아무것도 추가하지 않습니다. ( match2캐싱의 영향으로 인해 두 번의 연속 실행 사이에 약간의 속도가 나타납니다 .)
보다 "과학적인" 데이터 를 얻는 -ddump-simpl방법이 있습니다. 컴파일러가 코드를 보는 방식을 살펴볼 수 있습니다 .
중간 구조물의 검사는 우리의 친구입니다.
"핵심" 은 GHC의 내부 언어입니다. 모든 하스켈 소스 파일은 런타임 시스템이 실행하기위한 최종 기능 그래프로 변환되기 전에 코어로 단순화됩니다. 우리는이 중간 단계에서 보면, 그 우리를 말할 것 myButLast와 butLast2동일합니다. 이름 바꾸기 단계에서 모든 멋진 식별자가 무작위로 엉망이되기 때문에 살펴볼 필요가 있습니다.
% 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)
것으로 보인다 A1와 A4가장 유사하다. 철저한 검사를 통해 실제로 코드 구조 가 동일 A1하고 A4동일하다는 것을 알 수 있습니다. 둘 다 두 함수의 구성으로 정의되기 때문에 그 A2와 A3비슷합니다.
core출력을 광범위하게 살펴 보려면 and와 같은 플래그 도 제공 하는 것이 좋습니다 . 읽기가 훨씬 쉽습니다.-dsuppress-module-prefixes-dsuppress-uniques
적들의 짧은 목록도 있습니다.
그렇다면 벤치마킹 및 최적화에 어떤 문제가있을 수 있습니까?
ghci대화 형 재생 및 빠른 반복을 위해 설계된 Haskell 소스를 최종 실행 파일이 아닌 특정 바이트 코드로 컴파일하고 빠른 재로드를 위해 비싼 최적화를 피합니다.
- 프로파일 링은 개별 비트 및 복잡한 프로그램의 성능을 조사하는 데 유용한 도구처럼 보이지만 컴파일러 최적화를 심하게 손상시킬 수 있습니다.
- 귀하의 안전 장치는 자체 벤치 마크 러너와 함께 모든 작은 코드를 별도의 실행 파일로 프로파일 링하는 것입니다.
- 가비지 콜렉션을 조정할 수 있습니다. 바로 오늘 새로운 주요 기능이 릴리스되었습니다. 가비지 수집 지연은 예측하기 쉽지 않은 방식으로 성능에 영향을줍니다.
- 앞에서 언급했듯이 다른 컴파일러 버전은 다른 성능으로 다른 코드 를 작성하므로 약속을하기 전에 코드 사용자 가 코드를 작성하고 벤치마킹 할 때 사용할 버전을 알아야 합니다.
슬퍼 보일 수 있습니다. 그러나 대부분 Haskell 프로그래머와 관련이있는 것은 아닙니다. 실제 이야기 : 최근에 Haskell을 배우기 시작한 친구가 있습니다. 그들은 수치 통합을위한 프로그램을 작성했으며 거북이 느 렸습니다. 그래서 우리는 함께 앉아서 다이어그램과 내용으로 알고리즘에 대한 범주 설명을 썼습니다 . 그들이 추상적 인 설명과 일치하도록 코드를 다시 작성할 때, 그것은 마술처럼 치타처럼 빠르며 기억력이 떨어졌습니다. 우리는 즉시 π를 계산했습니다. 이야기의 교훈? 완벽한 추상 구조이며 코드가 자동으로 최적화됩니다.
init목록을 여러 번 "포장 풀기"하지 않도록 최적화되었습니다.