2011-12-28 2 views
18

Предупреждение о спойлере: это связано с Problem 14 от Project Euler.Почему этот простой алгоритм haskell настолько медленный?

Следующий код занимает около 15 секунд для запуска. У меня есть нерекурсивное решение Java, которое работает за 1 секунду. Я думаю, я должен был бы получить этот код намного ближе к этому.

import Data.List 

collatz a 1 = a 
collatz a x 
    | even x = collatz (a + 1) (x `div` 2) 
    | otherwise = collatz (a + 1) (3 * x + 1) 

main = do 
    print ((foldl1' max) . map (collatz 1) $ [1..1000000]) 

Я профилированный с +RHS -p и заметил, что выделенная память велика, и растет как входные растет. За n = 100,000 выделено 1gb (!), Для n = 1,000,000 13gb (!!).

С другой стороны, -sstderr показывает, что, хотя было выделено большое количество байтов, общий объем использования памяти составлял 1 мб, а производительность составляла 95% +, поэтому, возможно, 13 ГБ является красно-серой.

Я могу придумать несколько возможностей:

  1. Что-то не так строго, как это должно быть. Я уже обнаружил foldl1', но, может быть, мне нужно сделать больше? Можно отметить collatz , как строгий (это вообще имеет смысл?

  2. collatz не хвост вызова оптимизации. Я думаю, что это должно быть, но не знают способ, чтобы подтвердить.

  3. компилятор не делает некоторые оптимизации, я думаю, что это надо - например, только два результата collatz должны находиться в памяти в любой момент времени (макс и тока)

Любые предложения

?

Это почти дубликат Why is this Haskell expression so slow?, хотя я буду замечать, что быстрое решение Java не должно выполнять никаких заметок. Есть ли способы ускорить это, не прибегая к этому?

Для справки, вот мой выход профилирования:

Wed Dec 28 09:33 2011 Time and Allocation Profiling Report (Final) 

    scratch +RTS -p -hc -RTS 

    total time =  5.12 secs (256 ticks @ 20 ms) 
    total alloc = 13,229,705,716 bytes (excludes profiling overheads) 

COST CENTRE     MODULE    %time %alloc 

collatz      Main     99.6 99.4 


                           individual inherited 
COST CENTRE    MODULE            no. entries %time %alloc %time %alloc 

MAIN      MAIN             1   0 0.0 0.0 100.0 100.0 
CAF      Main             208   10 0.0 0.0 100.0 100.0 
    collatz    Main             215   1 0.0 0.0  0.0 0.0 
    main     Main             214   1 0.4 0.6 100.0 100.0 
    collatz    Main             216   0 99.6 99.4 99.6 99.4 
CAF      GHC.IO.Handle.FD          145   2 0.0 0.0  0.0 0.0 
CAF      System.Posix.Internals        144   1 0.0 0.0  0.0 0.0 
CAF      GHC.Conc            128   1 0.0 0.0  0.0 0.0 
CAF      GHC.IO.Handle.Internals        119   1 0.0 0.0  0.0 0.0 
CAF      GHC.IO.Encoding.Iconv        113   5 0.0 0.0  0.0 0.0 

И -sstderr:

./scratch +RTS -sstderr 
525 
    21,085,474,908 bytes allocated in the heap 
     87,799,504 bytes copied during GC 
      9,420 bytes maximum residency (1 sample(s))   
      12,824 bytes maximum slop    
       1 MB total memory in use (0 MB lost due to fragmentation) 

    Generation 0: 40219 collections,  0 parallel, 0.40s, 0.51s elapsed 
    Generation 1:  1 collections,  0 parallel, 0.00s, 0.00s elapsed 

    INIT time 0.00s ( 0.00s elapsed) 
    MUT time 35.38s (36.37s elapsed) 
    GC time 0.40s ( 0.51s elapsed) 
    RP time 0.00s ( 0.00s elapsed) PROF time 0.00s ( 0.00s elapsed) 
    EXIT time 0.00s ( 0.00s elapsed) 
    Total time 35.79s (36.88s elapsed) %GC time  1.1% (1.4% elapsed) Alloc rate 595,897,095 bytes per MUT second 

    Productivity 98.9% of total user, 95.9% of total elapsed 

И решение Java (не мое, взят из форумов Проекта Эйлера с запоминанием удалено):

public class Collatz { 
    public int getChainLength(int n) 
    { 
    long num = n; 
    int count = 1; 
    while(num > 1) 
    { 
     num = (num%2 == 0) ? num >> 1 : 3*num+1; 
     count++; 
    } 
    return count; 
    } 

    public static void main(String[] args) { 
    Collatz obj = new Collatz(); 
    long tic = System.currentTimeMillis(); 
    int max = 0, len = 0, index = 0; 
    for(int i = 3; i < 1000000; i++) 
    { 
     len = obj.getChainLength(i); 
     if(len > max) 
     { 
     max = len; 
     index = i; 
     } 
    } 
    long toc = System.currentTimeMillis(); 
    System.out.println(toc-tic); 
    System.out.println("Index: " + index + ", length = " + max); 
    } 
} 
+0

Удивительно, что GHC не оптимизирует (с n 2) по (rshift n 1), как и любой уважающий себя компилятор C. Есть ли причина? –

+0

@solrize: Меня это тоже удивило. – ehird

ответ

20

Сначала я подумал, что вы должны попробовать поставить восклицательный знак до в collatz:

collatz !a 1 = a 
collatz !a x 
    | even x = collatz (a + 1) (x `div` 2) 
    | otherwise = collatz (a + 1) (3 * x + 1) 

(Вам нужно поставить {-# LANGUAGE BangPatterns #-} в верхней части исходного файла для этой работы.)

Мои рассуждения пошли следующим образом: Проблема заключается в том, что вы создавая массивный thunk в первом аргументе для collatz: он начинается как 1, а затем становится 1 + 1, а затем становится (1 + 1) + 1, ... все без принуждения.Этот bang pattern заставляет первый аргумент collatz быть принудительным всякий раз, когда делается вызов, поэтому он начинается с 1, а затем становится равным 2 и т. Д., Не создавая большой неоцененный thunk: он просто остается как целое число.

Обратите внимание, что шаблон взлома является только сокращением для использования seq; в этом случае, мы могли бы переписать collatz следующим образом:

collatz a _ | seq a False = undefined 
collatz a 1 = a 
collatz a x 
    | even x = collatz (a + 1) (x `div` 2) 
    | otherwise = collatz (a + 1) (3 * x + 1) 

Хитрость здесь, чтобы заставить в охранника, который затем всегда имеет значение Ложь (и поэтому тело не имеет значения). Затем оценка продолжается со следующего случая: a уже оценен. Тем не менее, картина взлома более ясна.

К сожалению, при компиляции с -O2 это не работает быстрее, чем оригинал! Что еще мы можем попробовать? Ну, одна вещь, которую мы можем сделать, это предположить, что эти два числа не переполнение целого числа машин размера, и дать collatz этот тип аннотацию:

collatz :: Int -> Int -> Int 

Мы оставим шаблон взрыва там, так как мы должны избегать строительства даже если они не являются корнем проблемы производительности. Это сокращает время до 8,5 секунд на моем (медленном) компьютере.

Следующим шагом является попытка приблизиться к решению Java. Первое, что нужно понять, это то, что в Haskell div ведет себя более математически корректно по отношению к отрицательным целым числам, но медленнее, чем «нормальное» C-деление, которое в Haskell называется quot. Замена div на quot привела к сокращению времени до 5,2 секунд и замене x `quot` 2x `shiftR` 1 (импорт данных) на соответствие решению Java, которое сократило его до 4,9 секунд.

Это примерно так же мало, как я могу получить его сейчас, но я думаю, что это довольно хороший результат; поскольку ваш компьютер быстрее моего, он, надеюсь, будет еще ближе к решению Java.

Вот окончательный код (я немного очистки по пути):

{-# LANGUAGE BangPatterns #-} 

import Data.Bits 
import Data.List 

collatz :: Int -> Int 
collatz = collatz' 1 
    where collatz' :: Int -> Int -> Int 
     collatz' !a 1 = a 
     collatz' !a x 
      | even x = collatz' (a + 1) (x `shiftR` 1) 
      | otherwise = collatz' (a + 1) (3 * x + 1) 

main :: IO() 
main = print . foldl1' max . map collatz $ [1..1000000] 

Глядя на GHC сердечниками для этой программы (с ghc-core), я думаю, что это, вероятно, о том, как хорошо, как это получается; цикл collatz использует unboxed integers, а остальная часть программы выглядит нормально. Единственное улучшение, о котором я могу думать, это исключить бокс из итерации map collatz [1..1000000].

Кстати, не беспокойтесь о цифре «total alloc»; это общая память, выделенная за время жизни программы, и она никогда не уменьшается даже тогда, когда GC восстанавливает эту память. Цифры нескольких терабайт распространены.

+0

Спасибо, это действительно полезно. Я не знал о '-O2', что имеет огромное значение (сокращает время выполнения до 5 секунд). Добавлено решение Java на вопрос. –

+0

О, я предположил, что вы уже использовали '-O2', так как пересмотренная программа с шаблоном ударов заработала через 16 секунд на моей машине :) Я взгляну на ваше решение Java. – ehird

+0

ОК, я обновил этот ответ с дальнейшими улучшениями. – ehird

2

Вы можете потерять список и шаблоны ударов и получить тем же производительность, используя стек.

import Data.List 
import Data.Bits 

coll :: Int -> Int 
coll 0 = 0 
coll 1 = 1 
coll 2 = 2 
coll n = 
    let a = coll (n - 1) 
     collatz a 1 = a 
     collatz a x 
     | even x = collatz (a + 1) (x `shiftR` 1) 
     | otherwise = collatz (a + 1) (3 * x + 1) 
    in max a (collatz 1 n) 


main = do 
    print $ coll 100000 

Одна проблема состоит в том, что вам придется увеличить размер стека для больших входов, как 1_000_000.

обновление:

Вот хвост рекурсивной версии, которая не страдает от проблемы переполнения стека.

import Data.Word 
collatz :: Word -> Word -> (Word, Word) 
collatz a x 
    | x == 1 = (a,x) 
    | even x = collatz (a + 1) (x `quot` 2) 
    | otherwise = collatz (a + 1) (3 * x + 1) 

coll :: Word -> Word 
coll n = collTail 0 n 
    where 
    collTail m 1 = m 
    collTail m n = collTail (max (fst $ collatz 1 n) m) (n-1) 

Обратите внимание на использование Word вместо Int. Это отличает производительность. Вы все равно можете использовать шаблоны ударов, если хотите, и это почти удвоит производительность.

0

Одна вещь, которую я нашел, сделала удивительную разницу в этой проблеме. Я придерживался прямого рекуррентного отношения, а не складывался, вы должны простить выражение, подсчитывая его. Переписывание

collatz n = if even n then n `div` 2 else 3 * n + 1 

в

collatz n = case n `divMod` 2 of 
      (n', 0) -> n' 
      _  -> 3 * n + 1 

взял 1,2 секунды от времени выполнения для моей программы в системе с 2,8 ГГц Athlon II X4 с 430 CPU. Моя первая быстрая версия (2.3 секунды после использования divMod):

{-# LANGUAGE BangPatterns #-} 

import Data.List 
import Data.Ord 

collatzChainLen :: Int -> Int 
collatzChainLen n = collatzChainLen' n 1 
    where collatzChainLen' n !l 
      | n == 1 = l 
      | otherwise = collatzChainLen' (collatz n) (l + 1) 

collatz:: Int -> Int 
collatz n = case n `divMod` 2 of 
       (n', 0) -> n' 
       _  -> 3 * n + 1 

pairMap :: (a -> b) -> [a] -> [(a, b)] 
pairMap f xs = [(x, f x) | x <- xs] 

main :: IO() 
main = print $ fst (maximumBy (comparing snd) (pairMap collatzChainLen [1..999999])) 

возможно более идиоматических Haskell версия пробег примерно 9,7 секунды (8,5 с divMod); это идентично за исключением

collatzChainLen :: Int -> Int 
collatzChainLen n = 1 + (length . takeWhile (/= 1) . (iterate collatz)) n 

Использование Data.List.Stream должна позволить поток слияние, которое сделало бы эту версию бежать больше похоже, что с явным накоплением, но я не могу найти libghc * пакет Ubuntu, который имеет Data.List.Stream, поэтому я пока не могу это проверить.

Смежные вопросы