เมื่อศึกษาความเร็วและเพิ่มประสิทธิภาพของมันเป็นเรื่องง่ายมากที่จะได้รับผลที่ไม่ถูกต้องอย่างดุเดือด โดยเฉพาะอย่างยิ่งคุณไม่สามารถพูดได้ว่าตัวแปรหนึ่งตัวเร็วกว่าตัวแปรอื่นโดยไม่กล่าวถึงรุ่นคอมไพเลอร์และโหมดการปรับให้เหมาะสมของการตั้งค่าการเปรียบเทียบของคุณ ถึงกระนั้นโปรเซสเซอร์ที่ทันสมัยก็มีความซับซ้อนเช่นเดียวกับการคาดคะเนสาขาของโครงข่ายประสาทเทียมโดยไม่ต้องพูดถึงแคชทุกประเภทดังนั้นแม้จะตั้งค่าอย่างระมัดระวังผลการเปรียบเทียบจะเบลอ
ที่ถูกกล่าวว่า ...
การเปรียบเทียบเป็นเพื่อนของเรา
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
และดูวิธีที่คอมไพเลอร์เห็นรหัสของเรา
การตรวจสอบโครงสร้างกลางคือเพื่อนของเรา
"Core"เป็นภาษาภายในของ GHC ไฟล์ต้นฉบับ Haskell ทุกไฟล์จะถูกทำให้เป็น Core อย่างง่ายก่อนที่จะถูกแปลงเป็นกราฟการทำงานขั้นสุดท้ายเพื่อให้ระบบรันไทม์ทำงาน ถ้าเราดูที่กลางเวทีนี้ก็จะบอกเราว่า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
การส่งออกอย่างกว้างขวางก็จะทำให้ความรู้สึกที่จะยังจัดหาธงเช่นและ-dsuppress-module-prefixes
-dsuppress-uniques
พวกเขาทำให้อ่านง่ายขึ้นมาก
รายการสั้น ๆ ของศัตรูของเราเช่นกัน
ดังนั้นสิ่งที่ผิดพลาดกับการเปรียบเทียบและการเพิ่มประสิทธิภาพ
ghci
ถูกออกแบบมาสำหรับการเล่นแบบโต้ตอบและการวนซ้ำอย่างรวดเร็วรวบรวมแหล่ง Haskell กับรสชาติของรหัสไบต์แทนที่จะใช้งานขั้นสุดท้ายและหลีกเลี่ยงการเพิ่มประสิทธิภาพราคาแพงเพื่อรองรับการโหลดที่เร็วขึ้น
- การทำโปรไฟล์ดูเหมือนว่าเป็นเครื่องมือที่ดีในการดูประสิทธิภาพของแต่ละบิตและชิ้นส่วนของโปรแกรมที่ซับซ้อน แต่มันสามารถทำลายการเพิ่มประสิทธิภาพของคอมไพเลอร์ได้ไม่ดีผลที่ได้คือคำสั่งที่มีขนาดใหญ่
- การป้องกันของคุณคือการโพรไฟล์โค้ดเล็ก ๆ น้อย ๆ ในรูปแบบที่สามารถเรียกใช้งานได้แยกจากกันโดยมีเกณฑ์มาตรฐานของตัวเอง
- สามารถรวบรวมขยะได้ เมื่อไม่นานมานี้มีการเปิดตัวฟีเจอร์สำคัญใหม่ ความล่าช้าในการรวบรวมขยะจะส่งผลกระทบต่อประสิทธิภาพการทำงานในลักษณะที่ไม่สามารถคาดการณ์ได้อย่างตรงไปตรงมา
- ดังที่ได้กล่าวไปแล้วคอมไพเลอร์เวอร์ชันต่าง ๆ จะสร้างโค้ดที่แตกต่างกันพร้อมกับประสิทธิภาพที่แตกต่างกันดังนั้นคุณต้องรู้ว่าผู้ใช้รหัสของคุณจะใช้เวอร์ชันใดในการสร้างและเปรียบเทียบกับมาตรฐานนั้นก่อนที่คุณจะสัญญาใด ๆ
นี่อาจดูเศร้า แต่จริงๆแล้วไม่ใช่สิ่งที่ควรคำนึงถึงโปรแกรมเมอร์ Haskell ส่วนใหญ่แล้ว เรื่องจริง: ฉันมีเพื่อนที่เพิ่งเริ่มเรียนรู้ Haskell พวกเขาเขียนโปรแกรมเพื่อบูรณาการเชิงตัวเลขและเต่าก็ช้า ดังนั้นเราจึงนั่งลงด้วยกันและเขียนคำอธิบายประเภทของอัลกอริทึมพร้อมไดอะแกรมและเนื้อหา เมื่อพวกเขาเขียนโค้ดอีกครั้งเพื่อให้สอดคล้องกับคำอธิบายที่เป็นนามธรรมมันก็จะกลายเป็นอย่างเช่นเสือชีตาห์อย่างรวดเร็วและเพรียวบางในหน่วยความจำ เราคำนวณπในเวลาไม่นาน นิทานสอนใจ? โครงสร้างนามธรรมสมบูรณ์แบบและรหัสของคุณจะปรับให้เหมาะสม
init
ได้รับการปรับปรุงเพื่อหลีกเลี่ยงการ "เปิดออก" รายการหลายครั้ง