Подтвердить что ты не робот

Project Euler 14: производительность по сравнению с C и memoization

В настоящее время я работаю над проблемой проекта euler 14.

Я решил это с использованием плохо кодированной программы без заметок, которая запустила 386 5 секунд (см. править).

Вот он:

step :: (Integer, Int) -> Integer -> (Integer, Int)
step (i, m) n   | nextValue > m         = (n, nextValue)
                | otherwise             = (i, m)
                where nextValue = syr n 1

syr :: Integer -> Int -> Int
syr 1 acc   = acc
syr x acc   | even x    = syr (x `div` 2) (acc + 1)
            | otherwise = syr (3 * x + 1) (acc + 1)

p14 = foldl step (0, 0) [500000..999999]

Мой вопрос о нескольких комментариях в теме, связанной с этой проблемой, где упоминалось время выполнения < 1 s для следующих программ (код C, кредиты для пользователя форума проекта eiler ix для кода - примечание: я не проверял, что время выполнения фактически упоминается):

#include <stdio.h>


int main(int argc, char **argv) {
    int longest = 0;
    int terms = 0;
    int i;
    unsigned long j;
    for (i = 1; i <= 1000000; i++) {
        j = i;
        int this_terms = 1;
        while (j != 1) {
            this_terms++;
            if (this_terms > terms) {
                terms = this_terms;
                longest = i;
            }
            if (j % 2 == 0) {
                j = j / 2;
            } else {
                j = 3 * j + 1;
            }
        }
    }
    printf("longest: %d (%d)\n", longest, terms);
    return 0;
}

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

Так что я удивляюсь, почему существует такая большая разница? Или есть какая-то принципиальная разница между нашими двумя алгоритмами, которые могут оправдать фактор x6 в производительности?

Кстати, я в настоящее время пытаюсь реализовать этот алгоритм с помощью memoization, но я как бы потерял меня, его проще реализовать на императивном языке (и я еще не манипулирую монадами, поэтому я не могу использовать эта парадигма). Поэтому, если у вас есть хороший учебник, который подходит новичкам для изучения memoization, я буду рад (те, с которыми я столкнулся, были недостаточно подробными или из моей лиги).

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

Примечание2: любые общие советы о моем коде приветствуются.

РЕДАКТИРОВАТЬ: благодаря помощи delnan, я скомпилировал программу, и теперь она работает через 5 секунд, поэтому я в основном ищут подсказки по memoization (даже если идеи о существующем разрыве x6 по-прежнему приветствуются).

4b9b3361

Ответ 1

После компиляции с оптимизацией все еще есть несколько отличий от программы C

  • вы используете div, в то время как программа C использует разделение машины (которое усекает) [но любой уважающий себя компилятор C преобразует это в сдвиг, что делает его еще быстрее], это будет quot в Haskell; что сократило время работы примерно на 15%.
  • программа C использует 64-битную (или даже 32-разрядную) с фиксированной шириной, но тогда просто удача, что она получает правильный ответ, поскольку некоторые промежуточные значения превосходят 32-битные диапазоны), программа Haskell использует произвольную точность Integer s. Если в вашем GHC (64-разрядная ОС, отличная от Windows) есть 64-разрядный Int, замените Integer на Int. Это сократило время работы примерно в 3 раза. Если вы используете 32-битную систему, вам не повезло, GHC не использует собственные 64-битные инструкции там, эти операции реализованы как C-вызовы, которые все еще довольно медленные.

Для memoisation вы можете передать его в один из пакетов memoisation для хакаса, единственный, который я помню, data-memocombinators, но есть и другие. Или вы можете сделать это самостоятельно, например, сохранить карту ранее рассчитанных значений - это будет лучше всего работать в монаде State,

import Control.Monad.State.Strict
import qualified Data.Map as Map
import Data.Map (Map, singleton)

type Memo = Map Integer Int

syr :: Integer -> State Memo Int
syr n = do
    mb <- gets (Map.lookup n)
    case mb of
      Just l -> return l
      Nothing -> do
          let m = if even n then n `quot` 2 else 3*n+1
          l <- syr m
          let l' = l+1
          modify (Map.insert n l')
          return l'

solve :: Integer -> Int -> Integer -> State Memo (Integer,Int)
solve maxi len start
    | len > 1000000 = return (maxi,len)
    | otherwise = do
         l <- syr start
         if len < l
             then solve start l (start+1)
             else solve maxi len (start+1)

p14 :: (Integer,Int)
p14 = evalState (solve 0 0 500000) (singleton 1 1)

но это, вероятно, не будет слишком большим (даже если вы добавили необходимую строгость). Проблема в том, что поиск в Map не слишком дешев, а вставка относительно дорога.

Другой метод - сохранить измененный массив для поиска. Код становится более сложным, так как вам нужно выбрать разумную верхнюю границу для кеширования значений (должно быть не намного больше, чем оценка для начальных значений) и иметь дело с частями последовательностей, выходящих за пределы памяти. Но поиск и запись массива бывают быстрыми. Если у вас есть 64-разрядный Int s, приведенный ниже код работает довольно быстро, здесь он принимает 0.03s для лимита в 1 миллион и 0,33 для лимита в 10 миллионов, соответствующего (насколько я мог бы разумно) Код C работает в 0,018 или 0.2s.

module Main (main) where

import System.Environment (getArgs)
import Data.Array.ST
import Data.Array.Base
import Control.Monad.ST
import Data.Bits
import Data.Int

main :: IO ()
main = do
    args <- getArgs
    let bd = case args of
               a:_ -> read a
               _   -> 100000
    print $ collMax bd

next :: Int -> Int
next n
    | n .&. 1 == 0  = n `unsafeShiftR` 1
    | otherwise     = 3*n + 1

collMax :: Int -> (Int,Int16)
collMax upper = runST $ do
    arr <- newArray (0,upper) 0 :: ST s (STUArray s Int Int16)
    let go l m
            | upper < m = go (l+1) $ next m
            | otherwise = do
                l' <- unsafeRead arr m
                case l' of
                  0 -> do
                      l'' <- go 1 $ next m
                      unsafeWrite arr m (l'' + 1)
                      return (l+l'')
                  _ -> return (l+l'-1)
        collect mi ml i
            | upper < i = return (mi, ml)
            | otherwise = do
                l <- go 1 i
                if l > ml
                  then collect i l (i+1)
                  else collect mi ml (i+1)
    unsafeWrite arr 1 1
    collect 1 1 2

Ответ 2

Ну, программа C использует unsigned long, но Integer может хранить произвольно большие целые числа (это bignum). Если вы импортируете Data.Word, вы можете использовать Word, который является целым числом без знака машинного слова.

После замены Integer на Word и используя ghc -O2 и gcc -O3, программа C запускается через 0,72 секунды, а программы Haskell - за 1,92 секунды. 2.6x неплохо. Однако ghc -O2 не всегда помогает, и это одна из программ, на которых это не так! Используя только -O, как и вы, время выполнения сокращается до 1,90 секунд.

Я попытался заменить div на quot (который использует тот же тип деления, что и C, они отличаются только от отрицательных входов), но, как ни странно, это фактически запустило программу Haskell для меня немного медленнее.

Вы можете ускорить функцию syr с помощью этого предыдущего вопроса о переполнении стека Я ответил о той же проблеме Project Euler.

Ответ 3

В моей текущей системе (32-разрядный Core2Duo) ваш код Haskell, включая все оптимизации, указанные в ответах, принимает 0.8s для компиляции и 1.2s для запуска.

Вы можете передать время выполнения во время компиляции и сократить время выполнения до нуля.

module Euler14 where

import Data.Word
import Language.Haskell.TH

terms :: Word -> Word
terms n = countTerms n 0
  where
    countTerms 1 acc             = acc + 1
    countTerms n acc | even n    = countTerms (n `div` 2) (acc + 1)
                     | otherwise = countTerms (3 * n + 1) (acc + 1)

longestT :: Word -> Word -> (Word, Word) 
longestT mi mx = find mi mx (0, 0)
  where
      find mi mx (ct,cn) | mi == mx  = if ct > terms mi then (ct,cn) else (terms mi, mi)
                         | otherwise = find (mi + 1) mx
                                       (if ct > terms mi then (ct,cn) else (terms mi, mi))

longest :: Word -> Word -> ExpQ
longest mi mx = return $ TupE [LitE (IntegerL (fromIntegral a)),
                               LitE (IntegerL (fromIntegral b))]
  where
    (a,b) = longestT mi mx

и

{-# LANGUAGE TemplateHaskell #-}
import Euler14

main = print $(longest 500000 999999)

В моей системе для компиляции требуется 2.3s, но время выполнения сокращается до 0.003s. Выполнение функции времени компиляции (CTFE) - это то, что вы не можете сделать в C/С++. Единственным другим языком программирования, который я знаю о поддержке CTFE, является D язык программирования. И чтобы быть полным, код C принимает 0.1s для компиляции и 0.7s для запуска.