Khi nghiên cứu tốc độ và tối ưu hóa, rất dễ dàng để có được kết quả cực kỳ sai . Cụ thể, bạn thực sự không thể nói rằng một biến thể nhanh hơn biến thể khác mà không đề cập đến phiên bản trình biên dịch và chế độ tối ưu hóa của thiết lập điểm chuẩn của bạn. Ngay cả khi đó, các bộ xử lý hiện đại tinh vi đến mức có tính năng dự đoán nhánh dựa trên mạng thần kinh, chưa kể tất cả các loại bộ nhớ cache, do đó, ngay cả khi thiết lập cẩn thận, kết quả đo điểm chuẩn sẽ bị mờ.
Điều đó đang được nói ...
Điểm chuẩn là bạn của chúng tôi.
criterion
là một gói cung cấp các công cụ đo điểm chuẩn tiên tiến. Tôi nhanh chóng phác thảo một điểm chuẩn như thế này:
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 ]
Như bạn thấy, tôi đã thêm biến thể khớp rõ ràng vào hai yếu tố cùng một lúc, nhưng nếu không thì nó là cùng một mã nguyên văn. Tôi cũng chạy các điểm chuẩn theo chiều ngược lại, để nhận thức được sự sai lệch do bộ nhớ đệm. Vì vậy, chúng ta hãy chạy và xem!
% 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)
Có vẻ như phiên bản "chậm" của chúng tôi không hề chậm chút nào! Và sự phức tạp của khớp mẫu không thêm gì cả. (Tăng tốc một chút chúng ta thấy giữa hai lần chạy liên tiếp của match2
tôi quy cho các hiệu ứng của bộ nhớ đệm.)
Có một cách để có được dữ liệu "khoa học" hơn : chúng ta có thể -ddump-simpl
và xem cách trình biên dịch nhìn thấy mã của chúng ta.
Kiểm tra các cấu trúc trung gian là bạn của chúng tôi.
"Lõi" là ngôn ngữ nội bộ của GHC. Mỗi tệp nguồn Haskell được đơn giản hóa thành Core trước khi được chuyển thành biểu đồ chức năng cuối cùng để hệ thống thời gian chạy thực thi. Nếu chúng ta nhìn vào giai đoạn trung gian này, nó sẽ cho chúng ta biết điều đó myButLast
và butLast2
tương đương. Nó thực sự tìm kiếm, vì, ở giai đoạn đổi tên, tất cả các định danh đẹp của chúng tôi được thu thập ngẫu nhiên.
% 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)
Có vẻ như A1
và A4
là giống nhau nhất. Kiểm tra kỹ lưỡng sẽ cho thấy rằng thực sự các cấu trúc mã trong A1
và A4
giống hệt nhau. Điều đó A2
và A3
giống nhau cũng hợp lý vì cả hai đều được định nghĩa là một thành phần của hai chức năng.
Nếu bạn định kiểm tra core
đầu ra rộng rãi, thì cũng nên cung cấp các cờ như -dsuppress-module-prefixes
và -dsuppress-uniques
. Họ làm cho nó dễ đọc hơn nhiều.
Một danh sách ngắn của kẻ thù của chúng tôi, quá.
Vì vậy, những gì có thể đi sai với điểm chuẩn và tối ưu hóa?
ghci
, được thiết kế để chơi tương tác và lặp lại nhanh, biên dịch nguồn Haskell thành một hương vị nhất định của mã byte, thay vì thực thi cuối cùng, và tránh tối ưu hóa đắt tiền để ủng hộ tải lại nhanh hơn.
- Profiling có vẻ như là một công cụ tốt để xem xét hiệu suất của các bit và phần riêng lẻ của một chương trình phức tạp, nhưng nó có thể phá hỏng tối ưu hóa trình biên dịch rất tệ, kết quả sẽ là các đơn đặt hàng lớn.
- Bảo vệ của bạn là cấu hình từng bit mã nhỏ như một tệp thực thi riêng biệt, với trình chạy chuẩn của chính nó.
- Thu gom rác được điều chỉnh. Chỉ hôm nay một tính năng chính mới đã được phát hành. Sự chậm trễ cho việc thu gom rác sẽ ảnh hưởng đến hiệu suất theo những cách không dễ dự đoán.
- Như tôi đã đề cập, các phiên bản trình biên dịch khác nhau sẽ xây dựng mã khác nhau với hiệu suất khác nhau, vì vậy bạn phải biết phiên bản nào mà người dùng mã của bạn sẽ sử dụng để xây dựng mã đó và điểm chuẩn với điều đó, trước khi bạn thực hiện bất kỳ lời hứa nào.
Điều này có thể trông buồn. Nhưng nó thực sự không phải là điều mà một lập trình viên Haskell quan tâm, hầu hết thời gian. Câu chuyện có thật: Tôi có một người bạn vừa mới bắt đầu học Haskell. Họ đã viết một chương trình tích hợp số, và nó rất chậm. Vì vậy, chúng tôi ngồi xuống với nhau và đã viết một categorial mô tả của thuật toán, với sơ đồ và các công cụ. Khi họ viết lại mã để phù hợp với mô tả trừu tượng, nó trở nên kỳ diệu, giống như, cheetah nhanh và mỏng trên bộ nhớ. Chúng tôi tính toán π trong thời gian không. Đạo đức của câu chuyện? Cấu trúc trừu tượng hoàn hảo, và mã của bạn sẽ tự tối ưu hóa.
init
đã được tối ưu hóa để tránh "giải nén" danh sách nhiều lần.