How to improve the performance of this Haskell program?
我正在研究ProjectEuler中的问题,作为学习haskell的一种方式,我发现我的程序比类似的C版本慢得多,即使在编译时也是如此。我能做些什么来加快我的haskell程序?
例如,我对问题14的强力解决方案是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import Data.Int import Data.Ord import Data.List searchTo = 1000000 nextNumber :: Int64 -> Int64 nextNumber n | even n = n `div` 2 | otherwise = 3 * n + 1 sequenceLength :: Int64 -> Int sequenceLength 1 = 1 sequenceLength n = 1 + (sequenceLength next) where next = nextNumber n longestSequence = maximumBy (comparing sequenceLength) [1..searchTo] main = putStrLn $ show $ longestSequence |
这需要大约220秒,而"等效"的蛮力C版本只需要1.2秒。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | #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("%d ", longest); return 0; } |
号
我做错什么了?或者我天真地认为哈斯克尔甚至可以接近C的速度?
(我正在用gcc-o2编译C版本,用ghc-make-o编译haskell版本)。
为了测试目的,我刚刚设置了
用
使用一个累加器(你不需要sequencelength来变懒吧?)1.54秒。
1 2 3 4 5 6 | seqLen2 :: Int -> Integer -> Int seqLen2 a 1 = a seqLen2 a n = seqLen2 (a+1) (nextNumber n) sequenceLength :: Integer -> Int sequenceLength = seqLen2 1 |
使用
1 2 3 4 5 | nextNumber :: Integer -> Integer nextNumber n | r == 0 = q | otherwise = 6*q + 4 where (q,r) = quotRem n 2 |
号
使用Schwartzian变换代替
1 | longestSequence = snd $ maximum [(sequenceLength a, a) | a <- [1..searchTo]] |
注:
- 我通过使用
ghc -O 编译和使用+RTS -s 运行来检查时间。 - 我的机器在Mac OS X 10.6上运行。GHC版本为6.12.2。编译后的文件采用i386体系结构。)
- C问题以0.078s的速度运行,并带有相应的参数。它是用
gcc -O3 -m32 编译的。
尽管这已经相当古老了,让我插嘴一下,但有一个关键点以前没有被解决过。
首先,我盒子里不同节目的时间安排。由于我使用的是64位Linux系统,它们显示出一些不同的特性:使用
- C:0.3秒
- 原haskell:14.24秒,用
Integer 代替Int64 33.96秒 - KennyTM改进版:5.55秒,使用
Int 1.85秒 - Chris Kuklewicz的版本:5.73秒,使用
Int :1.90秒 - fuzzxl版本:3.56秒,使用
quotRem 而不是divMod :1.79秒
那我们有什么?
还缺少什么?
1 2 3 4 | if (j % 2 == 0) j = j / 2; else j = 3 * j + 1; |
我使用的任何C编译器都将测试
1 2 3 4 | nextNumber :: Integer -> Integer nextNumber n | fromInteger n .&. 1 == (0 :: Int) = n `quot` 2 | otherwise = 3*n+1 |
号
将运行时间缩短到3.25秒(注:对于
在
消除了列表的结构(C版本中也没有出现的结构)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | module Main (main) where import Data.Bits result :: Int result = findMax 0 0 1 findMax :: Int -> Int -> Int -> Int findMax start len can | can > 1000000 = start | canlen > len = findMax can canlen (can+1) | otherwise = findMax start len (can+1) where canlen = findLen 1 can findLen :: Int -> Int -> Int findLen l 1 = l findLen l n | n .&. 1 == 0 = findLen (l+1) (n `shiftR` 1) | otherwise = findLen (l+1) (3*n+1) main :: IO () main = print result |
产生更小的加速,导致运行时间为0.37秒。
因此,与C版本密切对应的haskell版本不会花那么长时间,它是~1.3的一个因子。
好吧,公平地说,C版本的效率很低,而Haskell版本没有这样的效率,
1 2 3 4 5 | if (this_terms > terms) { terms = this_terms; longest = i; } |
。
出现在内环中。在C版本中,将其从内部循环中取出会将运行时间缩短到0.27秒,使系数~1.4。
比较可能是重新计算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | type I = Integer data P = P {-# UNPACK #-} !Int {-# UNPACK #-} !I deriving (Eq,Ord,Show) searchTo = 1000000 nextNumber :: I -> I nextNumber n = case quotRem n 2 of (n2,0) -> n2 _ -> 3*n+1 sequenceLength :: I -> Int sequenceLength x = count x 1 where count 1 acc = acc count n acc = count (nextNumber n) (succ acc) longestSequence = maximum . map (\i -> P (sequenceLength i) i) $ [1..searchTo] main = putStrLn $ show $ longestSequence |
。
答案和计时比c慢,但它使用任意精度整数(通过
1 2 3 4 5 6 7 | ghc -O2 --make euler14-fgij.hs time ./euler14-fgij P 525 837799 real 0m3.235s user 0m3.184s sys 0m0.015s |
即使我有点晚了,这里是我的,我删除了对列表的依赖,这个解决方案也不使用堆。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | {-# LANGUAGE BangPatterns #-} -- Compiled with ghc -O2 -fvia-C -optc-O3 -Wall euler.hs module Main (main) where searchTo :: Int searchTo = 1000000 nextNumber :: Int -> Int nextNumber n = case n `divMod` 2 of (k,0) -> k _ -> 3*n + 1 sequenceLength :: Int -> Int sequenceLength n = sl 1 n where sl k 1 = k sl k x = sl (k + 1) (nextNumber x) longestSequence :: Int longestSequence = testValues 1 0 0 where testValues number !longest !longestNum | number > searchTo = longestNum | otherwise = testValues (number + 1) longest' longestNum' where nlength = sequenceLength number (longest',longestNum') = if nlength > longest then (nlength,number) else (longest,longestNum) main :: IO () main = print longestSequence |
。
我用
在这种情况下,编译器可以取消对所有
Haskell的列表是基于堆的,而您的C代码非常紧凑,根本不使用堆。您需要重构以删除对列表的依赖关系。