เมื่อค้นหาองค์ประกอบสุดท้าย แต่รายการที่สองทำไมจึงใช้ 'last` เร็วที่สุดในกลุ่มเหล่านี้


10

มี 3 ฟังก์ชั่นที่ระบุด้านล่างซึ่งค้นหาองค์ประกอบสุดท้าย แต่ที่สองในรายการ คนที่ใช้last . initดูเหมือนเร็วกว่าที่เหลือ ฉันดูเหมือนจะไม่เข้าใจว่าทำไม

สำหรับการทดสอบฉันใช้รายการอินพุต[1..100000000](100 ล้าน) อันสุดท้ายวิ่งเกือบจะทันทีในขณะที่คนอื่นใช้เวลาหลายวินาที

-- 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
initได้รับการปรับปรุงเพื่อหลีกเลี่ยงการ "เปิดออก" รายการหลายครั้ง
Willem Van Onsem

1
@WillemVanOnsem แต่ทำไมmyButLastช้ากว่ากัน? ดูเหมือนว่าไม่ได้เปิดออกรายการใด ๆ แต่เพียง traversing มันเป็นinitฟังก์ชั่นไม่ ...
38980

1
@Ismor: มันเป็น[x, y]สั้นสำหรับ(x:(y:[]))จึง unpacks ข้อเสียด้านนอกเป็นข้อเสียที่สองและการตรวจสอบถ้าหางของสองคือcons นอกจากนี้ข้อที่สองจะเปิดออกรายการอีกครั้งใน[] (x:xs)ใช่ว่าการเปิดกล่องบรรจุภัณฑ์มีประสิทธิภาพพอสมควร แต่แน่นอนว่าถ้าเกิดขึ้นบ่อยมากจะทำให้กระบวนการช้าลง
Willem Van Onsem

1
ดูที่hackage.haskell.org/package/base-4.12.0.0/docs/src/ …ดูเหมือนว่าการเพิ่มประสิทธิภาพนั้นinitไม่ได้ตรวจสอบซ้ำ ๆ ว่าอาร์กิวเมนต์นั้นเป็นรายการเดี่ยวหรือรายการว่างเปล่า เมื่อเริ่มต้นการสอบถามซ้ำเพียงแค่สมมติว่าองค์ประกอบแรกจะถูกตรึงไว้กับผลลัพธ์ของการเรียกซ้ำ
chepner

2
@WillemVanOnsem ฉันคิดว่าการเปิดออกอาจไม่ใช่ปัญหาที่นี่: GHC มีความเชี่ยวชาญในรูปแบบการโทรซึ่งจะให้รุ่นที่เหมาะที่สุดของคุณmyButLastโดยอัตโนมัติ ฉันคิดว่ามันน่าจะเป็นรายการฟิวชั่นที่จะโทษสำหรับการเร่งความเร็ว
oisdk

คำตอบ:


9

เมื่อศึกษาความเร็วและเพิ่มประสิทธิภาพของมันเป็นเรื่องง่ายมากที่จะได้รับผลที่ไม่ถูกต้องอย่างดุเดือด โดยเฉพาะอย่างยิ่งคุณไม่สามารถพูดได้ว่าตัวแปรหนึ่งตัวเร็วกว่าตัวแปรอื่นโดยไม่กล่าวถึงรุ่นคอมไพเลอร์และโหมดการปรับให้เหมาะสมของการตั้งค่าการเปรียบเทียบของคุณ ถึงกระนั้นโปรเซสเซอร์ที่ทันสมัยก็มีความซับซ้อนเช่นเดียวกับการคาดคะเนสาขาของโครงข่ายประสาทเทียมโดยไม่ต้องพูดถึงแคชทุกประเภทดังนั้นแม้จะตั้งค่าอย่างระมัดระวังผลการเปรียบเทียบจะเบลอ

ที่ถูกกล่าวว่า ...

การเปรียบเทียบเป็นเพื่อนของเรา

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 พวกเขาเขียนโปรแกรมเพื่อบูรณาการเชิงตัวเลขและเต่าก็ช้า ดังนั้นเราจึงนั่งลงด้วยกันและเขียนคำอธิบายประเภทของอัลกอริทึมพร้อมไดอะแกรมและเนื้อหา เมื่อพวกเขาเขียนโค้ดอีกครั้งเพื่อให้สอดคล้องกับคำอธิบายที่เป็นนามธรรมมันก็จะกลายเป็นอย่างเช่นเสือชีตาห์อย่างรวดเร็วและเพรียวบางในหน่วยความจำ เราคำนวณπในเวลาไม่นาน นิทานสอนใจ? โครงสร้างนามธรรมสมบูรณ์แบบและรหัสของคุณจะปรับให้เหมาะสม


ให้ข้อมูลอย่างมากและยังเป็นเรื่องเล็กน้อยสำหรับฉันในขั้นตอนนี้ ในกรณีนี้ "การเปรียบเทียบ" ทั้งหมดที่ฉันทำคือเรียกใช้ฟังก์ชั่นทั้งหมดสำหรับรายการ 100 ล้านรายการและสังเกตว่าหนึ่งใช้เวลานานกว่าอีกรายการหนึ่ง เกณฑ์มาตรฐานที่มีเกณฑ์ค่อนข้างมีประโยชน์ นอกจากนี้ghciดูเหมือนว่าจะให้ผลลัพธ์ที่แตกต่าง (ในแง่ของความเร็ว) เมื่อเทียบกับการทำ exe ก่อนเช่นคุณกล่าวว่า
storm125
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.