เครื่องมือสำหรับวิเคราะห์ประสิทธิภาพของโปรแกรม Haskell


104

ขณะที่การแก้ปัญหาโครงการออยเลอร์ในการเรียนรู้ Haskell (ดังนั้นขณะนี้ฉันเริ่มต้นสมบูรณ์) ฉันมามากกว่าปัญหา 12 ฉันเขียนวิธีแก้ปัญหา (ไร้เดียงสา) นี้:

--Get Number of Divisors of n
numDivs :: Integer -> Integer
numDivs n = toInteger $ length [ x | x<-[2.. ((n `quot` 2)+1)], n `rem` x == 0] + 2

--Generate a List of Triangular Values
triaList :: [Integer]
triaList =  [foldr (+) 0 [1..n] | n <- [1..]]

--The same recursive
triaList2 = go 0 1
  where go cs n = (cs+n):go (cs+n) (n+1)

--Finds the first triangular Value with more than n Divisors
sol :: Integer -> Integer
sol n = head $ filter (\x -> numDivs(x)>n) triaList2

วิธีแก้ปัญหาn=500 (sol 500)นี้ช้ามาก (ใช้งานได้นานกว่า 2 ชั่วโมงแล้ว) ดังนั้นฉันจึงสงสัยว่าจะทราบได้อย่างไรว่าเหตุใดโซลูชันนี้จึงช้ามาก มีคำสั่งใดบ้างที่บอกฉันว่าเวลาในการคำนวณส่วนใหญ่ใช้ไปที่ไหนดังนั้นฉันจึงรู้ว่าโปรแกรม haskell ของฉันทำงานช้าหรือไม่ บางอย่างเช่นผู้สร้างโปรไฟล์ธรรมดา

จะทำให้มันชัดเจนผมไม่ได้ขอสำหรับการแก้ปัญหาได้เร็วขึ้น แต่สำหรับวิธีที่จะหาวิธีการแก้ปัญหานี้ คุณจะเริ่มยังไงถ้าคุณไม่มีความรู้เรื่องแฮสเคล?

ฉันพยายามเขียนสองtriaListฟังก์ชัน แต่ไม่พบวิธีทดสอบว่าอันไหนเร็วกว่าดังนั้นนี่คือจุดเริ่มต้นของปัญหา

ขอบคุณ

คำตอบ:


187

จะทราบได้อย่างไรว่าเหตุใดวิธีนี้จึงช้ามาก มีคำสั่งใดบ้างที่บอกฉันว่าเวลาส่วนใหญ่ในการคำนวณใช้เวลาส่วนไหนดังนั้นฉันจึงรู้ว่าโปรแกรม haskell ของฉันทำงานช้าหรือไม่

แม่นยำ! GHC มีเครื่องมือที่ยอดเยี่ยมมากมาย ได้แก่ :

กวดวิชาเกี่ยวกับการใช้เวลาและพื้นที่ profiling เป็นส่วนหนึ่งของโลกแห่งความจริง Haskell

สถิติ GC

ประการแรกตรวจสอบให้แน่ใจว่าคุณกำลังคอมไพล์ด้วย ghc -O2 และคุณอาจแน่ใจว่าเป็น GHC สมัยใหม่ (เช่น GHC 6.12.x)

สิ่งแรกที่เราทำได้คือตรวจสอบว่าการเก็บขยะไม่ใช่ปัญหา รันโปรแกรมของคุณด้วย + RTS -s

$ time ./A +RTS -s
./A +RTS -s 
749700
   9,961,432,992 bytes allocated in the heap
       2,463,072 bytes copied during GC
          29,200 bytes maximum residency (1 sample(s))
         187,336 bytes maximum slop
               **2 MB** total memory in use (0 MB lost due to fragmentation)

  Generation 0: 19002 collections,     0 parallel,  0.11s,  0.15s elapsed
  Generation 1:     1 collections,     0 parallel,  0.00s,  0.00s elapsed

  INIT  time    0.00s  (  0.00s elapsed)
  MUT   time   13.15s  ( 13.32s elapsed)
  GC    time    0.11s  (  0.15s elapsed)
  RP    time    0.00s  (  0.00s elapsed)
  PROF  time    0.00s  (  0.00s elapsed)
  EXIT  time    0.00s  (  0.00s elapsed)
  Total time   13.26s  ( 13.47s elapsed)

  %GC time       **0.8%**  (1.1% elapsed)

  Alloc rate    757,764,753 bytes per MUT second

  Productivity  99.2% of total user, 97.6% of total elapsed

./A +RTS -s  13.26s user 0.05s system 98% cpu 13.479 total

ซึ่งให้ข้อมูลมากมายกับเราอยู่แล้ว: คุณมีฮีปเพียง 2M เท่านั้นและ GC ใช้เวลาถึง 0.8% จึงไม่ต้องกังวลว่าการจัดสรรจะเป็นปัญหา

โปรไฟล์เวลา

การรับโปรไฟล์เวลาสำหรับโปรแกรมของคุณนั้นตรงไปตรงมา: คอมไพล์ด้วย -prof -auto-all

 $ ghc -O2 --make A.hs -prof -auto-all
 [1 of 1] Compiling Main             ( A.hs, A.o )
 Linking A ...

และสำหรับ N = 200:

$ time ./A +RTS -p                   
749700
./A +RTS -p  13.23s user 0.06s system 98% cpu 13.547 total

ซึ่งสร้างไฟล์ A.prof ที่มี:

    Sun Jul 18 10:08 2010 Time and Allocation Profiling Report  (Final)

       A +RTS -p -RTS

    total time  =     13.18 secs   (659 ticks @ 20 ms)
    total alloc = 4,904,116,696 bytes  (excludes profiling overheads)

COST CENTRE          MODULE         %time %alloc

numDivs            Main         100.0  100.0

บ่งชี้ว่าเวลาทั้งหมดของคุณใช้ไปใน numDivs และยังเป็นที่มาของการจัดสรรทั้งหมดของคุณ

Heap Profiles

นอกจากนี้คุณยังสามารถแบ่งการจัดสรรเหล่านั้นได้ด้วยการรันด้วย + RTS -p -hy ซึ่งจะสร้าง A.hp ซึ่งคุณสามารถดูได้โดยการแปลงเป็นไฟล์ Postscript (hp2ps -c A.hp) โดยสร้าง:

ข้อความแสดงแทน

ซึ่งบอกเราว่าไม่มีอะไรผิดปกติกับการใช้หน่วยความจำของคุณ: มันกำลังจัดสรรในพื้นที่คงที่

ดังนั้นปัญหาของคุณคือความซับซ้อนของอัลกอริทึมของ numDivs:

toInteger $ length [ x | x<-[2.. ((n `quot` 2)+1)], n `rem` x == 0] + 2

แก้ไขสิ่งนั้นซึ่งเป็นเวลาวิ่ง 100% ของคุณและทุกอย่างก็ทำได้ง่าย

การเพิ่มประสิทธิภาพ

นิพจน์นี้เป็นตัวเลือกที่ดีสำหรับการเพิ่มประสิทธิภาพสตรีมฟิวชั่นดังนั้นฉันจะเขียนใหม่เพื่อใช้Data.Vectorดังนี้:

numDivs n = fromIntegral $
    2 + (U.length $
        U.filter (\x -> fromIntegral n `rem` x == 0) $
        (U.enumFromN 2 ((fromIntegral n `div` 2) + 1) :: U.Vector Int))

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

การทดสอบนี้ ghc -O2 - สร้าง Z.hs

$ time ./Z     
749700
./Z  3.73s user 0.01s system 99% cpu 3.753 total

ดังนั้นมันจึงลดเวลาในการทำงานสำหรับ N = 150 ลง 3.5x โดยไม่ต้องเปลี่ยนอัลกอริทึมเอง

สรุป

ปัญหาของคุณคือ numDivs เป็นเวลาวิ่งของคุณ 100% และมีความซับซ้อนมาก ลองนึกถึง numDivs และตัวอย่างเช่นสำหรับแต่ละ N ที่คุณสร้าง [2 .. n div2 + 1] N ครั้ง ลองจดบันทึกเนื่องจากค่าต่างๆไม่เปลี่ยนแปลง

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


ภาคผนวก

เนื่องจาก numDivs คือ 100% ของเวลาทำงานของคุณการสัมผัสส่วนอื่น ๆ ของโปรแกรมจะไม่สร้างความแตกต่างมากนักอย่างไรก็ตามเพื่อวัตถุประสงค์ในการเรียนการสอนเรายังสามารถเขียนซ้ำโดยใช้การหลอมรวมของสตรีม

นอกจากนี้เรายังสามารถเขียน trialList ใหม่และพึ่งพาฟิวชั่นเพื่อเปลี่ยนเป็นลูปที่คุณเขียนด้วยมือใน trialList2 ซึ่งเป็นฟังก์ชัน "สแกนคำนำหน้า" (aka scanl):

triaList = U.scanl (+) 0 (U.enumFrom 1 top)
    where
       top = 10^6

ในทำนองเดียวกันสำหรับโซล:

sol :: Int -> Int
sol n = U.head $ U.filter (\x -> numDivs x > n) triaList

ด้วยเวลาทำงานโดยรวมที่เท่ากัน แต่รหัสที่สะอาดกว่าเล็กน้อย


หมายเหตุสำหรับคนงี่เง่าคนอื่น ๆ เช่นฉันtimeยูทิลิตี้ที่ Don กล่าวถึงใน Time Profiles เป็นเพียงtimeโปรแกรมLinux ไม่มีใน Windows ดังนั้นสำหรับเวลาโปรไฟล์บน Windows (ทุกที่จริง) เห็นนี้คำถาม
John Red

1
สำหรับผู้ใช้ในอนาคตจะเลิกในความโปรดปรานของ-auto-all -fprof-auto
B.Mehta

60

คำตอบของ Dons นั้นยอดเยี่ยมมากโดยไม่ต้องสปอยล์ด้วยการให้วิธีแก้ปัญหาโดยตรง
ที่นี่ฉันต้องการแนะนำเครื่องมือเล็ก ๆ น้อย ๆที่ฉันเขียนเมื่อเร็ว ๆ นี้ มันช่วยให้คุณประหยัดเวลาในการเขียนคำอธิบายประกอบ SCC ghc -prof -auto-allด้วยมือเมื่อคุณต้องการรายละเอียดที่ละเอียดมากขึ้นกว่าค่าเริ่มต้น นอกจากนั้นยังมีสีสัน!

นี่คือตัวอย่างรหัสที่คุณให้ (*) สีเขียวใช้ได้สีแดงช้า: ข้อความแสดงแทน

ตลอดเวลาในการสร้างรายการตัวหาร สิ่งนี้แนะนำบางสิ่งที่คุณทำได้:
1. ทำให้การกรองn rem x == 0เร็วขึ้น แต่เนื่องจากเป็นฟังก์ชันในตัวอาจจะเร็วอยู่แล้ว
2. สร้างรายการที่สั้นกว่า n quot 2คุณได้ทำมาแล้วบางสิ่งบางอย่างไปในทิศทางที่โดยการตรวจสอบเท่านั้นถึง
3. ทิ้งการสร้างรายการโดยสิ้นเชิงและใช้คณิตศาสตร์เพื่อหาวิธีแก้ปัญหาที่เร็วขึ้น นี่เป็นวิธีปกติสำหรับปัญหาของโครงการออยเลอร์

(*) ผมได้รับนี้โดยการใส่รหัสของคุณในไฟล์ที่เรียกว่าการเพิ่มฟังก์ชั่นหลักeu13.hs main = print $ sol 90แล้วการทำงานและผลที่อยู่ในvisual-prof -px eu13.hs eu13eu13.hs.html


3

หมายเหตุที่เกี่ยวข้องกับ Haskell: triaList2แน่นอนว่าเร็วกว่าtriaListเพราะช่วงหลังทำการคำนวณที่ไม่จำเป็นจำนวนมาก มันจะใช้เวลาในการคำนวณสมการกำลังสอง n องค์ประกอบแรกของแต่เชิงเส้นสำหรับtriaList triaList2มีอีกวิธีหนึ่งที่สวยงาม (และมีประสิทธิภาพ) ในการกำหนดรายการตัวเลขสามเหลี่ยมที่ไม่สิ้นสุด:

triaList = 1 : zipWith (+) triaList [2..]

หมายเหตุที่เกี่ยวข้องกับคณิตศาสตร์: ไม่จำเป็นต้องตรวจสอบตัวหารทั้งหมดถึง n / 2 ก็เพียงพอที่จะตรวจสอบได้ถึง sqrt (n)


2
พิจารณาด้วย: scanl (+) 1 [2 .. ]
Don Stewart

1

คุณสามารถรันโปรแกรมของคุณด้วยแฟล็กเพื่อเปิดใช้งานการทำโปรไฟล์เวลา สิ่งนี้:

./program +RTS -P -sprogram.stats -RTS

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


1
แต่รวบรวมไว้ก่อนด้วยghc -prof -auto-all -fforce-recomp --make -O2 program.hs
Daniel
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.