项目Euler 14:与C和memoization相比的性能

时间:2012-03-18 18:29:02

标签: haskell memoization

我目前正致力于project euler problem 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]

我的问题是关于这个问题的线程中的几条评论,其中提到的程序的执行时间为&lt; 1秒如下(C代码,项目euler论坛用户的信用 ix < / em> 代码 - 注意:我没有检查执行时间实际上是如上所述):

#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实现这个算法,但是对我来说有点迷失,用命令式语言实现起来更容易(而且我还没有操纵monad所以我不能使用这个范例)。所以,如果你有任何适合初学者学习备忘录的好教程,我会很高兴(我遇到的那些不够详细或不在我的联盟中)。

注意:我通过Prolog来进行声明式范例,并且仍处于发现Haskell的早期阶段,所以我可能会错过重要的事情。

注2:欢迎任何关于我的代码的一般建议。

编辑:感谢delnan的帮助,我编译了程序,它现在运行5秒钟,所以我现在主要寻找关于memoization的提示(即使仍然欢迎有关现有x6差距的想法)。

3 个答案:

答案 0 :(得分:9)

在使用优化进行编译之后,C程序仍存在一些差异

  • 你使用div,而C程序使用机器划分(截断)[但任何自尊的C编译器将其转换为移位,这样使它更快],那将是{{1在哈斯克尔;这使得运行时间减少了大约15%。
  • C程序使用固定宽度64位(甚至32位,但它只是运气得到正确的答案,因为一些中间值超过32位范围)整数,Haskell程序使用任意精度quot秒。如果您的GHC(Windows以外的64位操作系统)中有64位Integer,请将Int替换为Integer。这样可以将运行时间缩短约3倍。如果您使用的是32位系统,那么运气不好,GHC不会在那里使用本机64位指令,这些操作是作为C调用实现的,但仍然很慢。

对于备忘录,您可以将其外包给hackage上的一个记忆包,我唯一记得的是data-memocombinators,但还有其他一些。或者你可以自己做,例如保留以前计算的值的地图 - 这在Int monad中效果最好,

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) 中的查找不是太便宜而且插入相对昂贵。

另一种方法是为查找保留一个可变数组。代码变得更加复杂,因为您必须为要缓存的值选择合理的上限(应该不比起始值的界限大很多)并处理落在备忘范围之外的序列部分。但是数组查找和写入速度很快。如果你有64位Map s,下面的代码运行得非常快,这里需要0.03s,限制为100万,0.33s限制为1000万,相应(尽可能合理) can)C代码以0.018分别运行。 0.2S。

Int

答案 1 :(得分:4)

好吧,C程序使用unsigned long,但Integer可以存储任意大整数(它是bignum)。如果您导入Data.Word,则可以使用Word,这是一个机器字大小的无符号整数。

Integer替换Word并使用ghc -O2gcc -O3后,C程序在0.72秒内运行,而Haskell程序在1.92秒内运行。 2.6x也不错。但是,ghc -O2并不总是有用,而且这是它没有的程序之一!像你一样只使用-O,将运行时间缩短到1.90秒。

我尝试用div替换quot(它使用与C相同的除法类型;它们只在负输入上有所不同),但奇怪的是它实际上使Haskell程序对我来说运行得稍慢。

你应该能够在this previous Stack Overflow question的帮助下加快syr功能。我回答了同样的Project Euler问题。

答案 2 :(得分:2)

在我当前的系统(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 / C ++中无法做到的。我所知道的唯一支持CTFE的编程语言是D programming language。只是为了完成,C代码需要0.1s来编译并0.7s来运行。