як дізнатися, чому це рішення так повільно. Чи є команди, які підказують мені, де витрачається більша частина часу на обчислення, щоб я знав, яка частина моєї програми 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), генеруючи:
що говорить нам, що нічого поганого у використанні вашої пам’яті немає: вона розподіляється у постійному просторі.
Отже, вашою проблемою є алгоритмічна складність 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 div
2 + 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 (будь-де фактично) дивіться це питання.