Інструменти для аналізу продуктивності програми 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 повільна? Щось на зразок простого профілера.

Для того, щоб зрозуміти, я не прошу для більш швидкого вирішення , але і для шляху , щоб знайти це рішення. Як би ви почали, якщо б у вас не було знань haskell?

Я спробував написати дві triaListфункції, але не знайшов способу перевірити, яка з них швидша, тож саме тут починаються мої проблеми.

Дякую

Відповіді:


187

як дізнатися, чому це рішення так повільно. Чи є команди, які підказують мені, де витрачається більша частина часу на обчислення, щоб я знав, яка частина моєї програми haskell повільна?

Точно! GHC пропонує безліч чудових інструментів, серед яких:

Підручник по використанню профілю часу та простору є частиною Real World Haskell .

Статистика GC

По-перше, переконайтеся, що ви компілюєте з ghc -O2. І ви можете переконатися, що це сучасний GHC (наприклад, GHC 6.12.x)

Перше, що ми можемо зробити, це перевірити, чи не є проблемою збирання сміття. Запустіть свою програму за допомогою + RTS

$ 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

Це вже дає нам багато інформації: у вас є лише купа 2М, а 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, а також є джерелом усіх ваших асигнувань.

Купи профілів

Ви також можете отримати розбиття цих виділень, запустивши з + RTS -p -hy, що створює A.hp, який ви можете переглянути, перетворивши його у файл постскрипту (hp2ps -c A.hp), генеруючи:

alt текст

що говорить нам, що нічого поганого у використанні вашої пам’яті немає: вона розподіляється у постійному просторі.

Отже, вашою проблемою є алгоритмічна складність 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,5х, не змінюючи самого алгоритму.

Висновок

Ваша проблема - numDivs. Це 100% вашого часу роботи і має жахливі складності. Подумайте про numDivs і як, наприклад, для кожного N, який ви генеруєте [2 .. n div2 + 1] N разів. Спробуйте запам'ятати це, оскільки значення не змінюються.

Щоб виміряти, яка з ваших функцій швидша, розгляньте використання критерію , який надасть статистично надійну інформацію про полімікросекундні покращення часу роботи.


Додатки

Оскільки numDivs становить 100% вашого робочого часу, торкання інших частин програми не матиме великої різниці, проте для педагогічних цілей ми також можемо переписати тих, хто використовує потокове синтез.

Ми також можемо переписати trialList і покластися на fusion, щоб перетворити його на цикл, який ви пишете від руки в trialList2, що є функцією "сканування префікса" (aka scanl):

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

Аналогічно для sol:

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

З тим же загальним часом роботи, але трохи більш чистим кодом.


Просто примітка для інших ідіотів, як я: timeУтиліта, яку Дон згадував у Time Profiles, - це лише timeпрограма Linux . Він не доступний у Windows. Тож про час профілювання в Windows (будь-де фактично) дивіться це питання.
Джон Ред

1
Для майбутніх користувачів -auto-allзастаріла на користь -fprof-auto.
Б. Мехта

60

Відповідь Донса чудова, не будучи спойлером, даючи пряме рішення проблеми.
Тут я хочу запропонувати невеликий інструмент, про який я писав недавно. Це економить вам час для написання анотацій SCC вручну, коли ви хочете більш детальний профіль, ніж за замовчуванням ghc -prof -auto-all. Крім того, що це барвисто!

Ось приклад з кодом, який ви дали (*), зелений колір ОК, червоний - повільний: alt текст

Весь час йде на створення списку дільників. Це говорить про кілька дій, які ви можете зробити:
1. Зробити фільтрацію n rem x == 0швидше, але оскільки це вбудована функція, ймовірно, вона вже швидка.
2. Створіть коротший список. Ви вже зробили щось у цьому напрямку, перевіривши лише до n quot 2.
3. Викиньте генерування списку повністю і використовуйте трохи математики, щоб отримати швидше рішення. Це звичайний спосіб вирішення проблем Ейлера.

(*) Я отримав це, помістивши ваш код у названий файл eu13.hs, додавши головну функцію main = print $ sol 90. Потім біг visual-prof -px eu13.hs eu13і результат в eu13.hs.html.


3

Примітка Haskell: triaList2звичайно швидше, ніж triaListчерез те, що останній виконує багато зайвих обчислень. Пройде обчислення n перших елементів triaList, але лінійних для квадратичного часу triaList2. Є ще один елегантний (і ефективний) спосіб визначити нескінченний ледачий список номерів трикутників:

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

Примітка, пов'язана з математикою: не потрібно перевіряти всі дільники до n / 2, достатньо перевірити до sqrt (n).


2
Також врахуйте: scanl (+) 1 [2 ..]
Дон Стюарт

1

Ви можете запустити свою програму прапорами, щоб увімкнути час профілювання. Щось на зразок цього:

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

Це має запустити програму і створити файл під назвою program.stats, який матиме кількість часу, витраченого на кожну функцію. Ви можете знайти більше інформації про профілювання з GHC в посібнику користувача GHC . Для порівняльного аналізу існує бібліотека критеріїв. Я виявив, що ця публікація є корисною.


1
Але спочатку компілюйте це зghc -prof -auto-all -fforce-recomp --make -O2 program.hs
Даніель,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.