Почему этот простой алгоритм на Haskell такой медленный? - PullRequest
19 голосов
/ 28 декабря 2011

Оповещение о спойлере: это связано с Проблема 14 от Project Euler.

Для запуска следующего кода требуется около 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 1 ГБ выделено (!), Для n = 1,000,000 13 ГБ (!!) выделено.

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

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

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

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

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

Есть предложения?

Это в значительной степени дубликат Почему это выражение на Haskell такое медленное? , хотя я отмечучто быстрое 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 (не мое, взято с форумов Project Euler с удаленной памяткой):

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 );
  }
}

Ответы [ 3 ]

21 голосов
/ 28 декабря 2011

Сначала я подумал, что вы должны попробовать поставить восклицательный знак перед a в 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, ... все без принуждения.Этот шаблон взрыва заставляет первый аргумент 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 в охране, которая затем всегда оценивается как Ложь (и поэтому телоне имеет значения).Затем оценка продолжается со следующего случая: a уже был оценен.Тем не менее, шаблон взрыва более ясен.

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

collatz :: Int -> Int -> Int

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

Следующий шаг - попытаться приблизить это к решению Java.Первое, что нужно понять, это то, что в Haskell div ведет себя более математически правильно по отношению к отрицательным целым числам, но медленнее, чем «нормальное» деление C, которое в Haskell называется quot.Замена div на quot сократила время выполнения до 5,2 секунды, а замена x `quot` 2 на x `shiftR` 1 (импорт Data.Bits) для соответствия решению 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 использует целые числа без коробки, а остальная часть программы выглядит нормально.Единственное улучшение, которое я могу придумать, - это исключить бокс из итерации map collatz [1..1000000].

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

2 голосов
/ 29 декабря 2011

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

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 голосов
/ 22 ноября 2013

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

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 секунды времени выполнения моей программы в системе с процессором Athlon II X4 430 с частотой 2,8 ГГц.Моя первоначальная более быстрая версия (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 должно позволить потоковое объединение, которое сделало бы эту версию более похожей с явным накоплением, но я не могу найти пакет Ubuntu libghc *в нем есть Data.List.Stream, поэтому я пока не могу это проверить.

...