我正在研究Project Euler中的问题,作为学习Haskell的一种方式,我发现我的程序比同类C版本慢得多,即使在编译时也是如此。我该怎么做才能加速我的Haskell程序?
例如,我对Problem 14的强力解决方案是:
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秒。
#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\n", longest);
return 0;
}
我做错了什么?或者我天真地认为Haskell甚至可以接近C的速度?
(我正在使用gcc -O2编译C版本,使用ghc -make -O编译Haskell版本。)
答案 0 :(得分:24)
出于测试目的,我刚刚设置searchTo = 100000
。所用时间 7.34s 。一些修改会带来一些重大改进:
使用Integer
代替Int64
。这样可以缩短 1.75s 的时间。
使用累加器(你不需要sequenceLength对吗?) 1.54s 。
seqLen2 :: Int -> Integer -> Int
seqLen2 a 1 = a
seqLen2 a n = seqLen2 (a+1) (nextNumber n)
sequenceLength :: Integer -> Int
sequenceLength = seqLen2 1
使用nextNumber
重写quotRem
,从而避免计算两次除法(一次在even
,一次在div
)。的 1.27s 强>
nextNumber :: Integer -> Integer
nextNumber n
| r == 0 = q
| otherwise = 6*q + 4
where (q,r) = quotRem n 2
使用Schwartzian transform代替maximumBy
。 maximumBy . comparing
的问题是每个值都会多次调用sequenceLength
函数。的 0.32s 强>
longestSequence = snd $ maximum [(sequenceLength a, a) | a <- [1..searchTo]]
注意:
ghc -O
进行编译并使用+RTS -s
)gcc -O3 -m32
编译。答案 1 :(得分:11)
虽然这已经相当陈旧了,但请允许我说一下,之前还没有解决过一个至关重要的问题。
首先,我的盒子上的不同程序的时间。由于我使用的是64位Linux系统,它们显示出一些不同的特性:使用Integer
代替Int64
并不会像使用32位GHC那样提高性能,其中每个{ {1}}操作会产生C调用的成本,而Int64
符合有符号32位整数的计算不需要外来调用(因为这里只有少数操作超出该范围, Integer
是32位GHC上更好的选择。
Integer
代替Integer
:33.96秒Int64
:1.85秒Int
:1.90秒Int
代替quotRem
:1.79秒那我们有什么?
divMod
resp。 div
如果没有必要divMod
,请{ quot
要快得多还缺少什么?
quotRem
我使用的任何C编译器都将测试if (j % 2 == 0)
j = j / 2;
else
j = 3 * j + 1;
转换为位掩码,并且不使用除法指令。 GHC(尚未)这样做。因此,测试j % 2 == 0
或计算even n
是一项非常昂贵的操作。使用
n `quotRem` 2
版本中的nextNumber
Integer
将其运行时间缩短至3.25秒(注意:对于nextNumber :: Integer -> Integer
nextNumber n
| fromInteger n .&. 1 == (0 :: Int) = n `quot` 2
| otherwise = 3*n+1
,Integer
比n `quot` 2
快,需要12.69秒!)。
在n `shiftR` 1
版本中执行相同操作会将其运行时间缩短至0.41秒。对于Int
s,除以2的位移比Int
操作快一点,将其运行时间减少到0.39秒。
消除列表的构建(也不会出现在C版本中),
quot
产生更小的加速,导致0.37秒的运行时间。
因此,与C版本密切对应的Haskell版本不会花费更长的时间,它是〜1.3的一个因素。
嗯,让我们公平一点,C版本中的效率低下,Haskell版本中没有,
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
出现在内循环中。将其从C版本的内环中取出可将其运行时间缩短至0.27秒,使得系数达到1.4。
答案 2 :(得分:5)
比较可能会重新计算sequenceLength
太多。这是我最好的版本:
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慢,但它确实使用任意精度整数(通过Integer
类型):
ghc -O2 --make euler14-fgij.hs
time ./euler14-fgij
P 525 837799
real 0m3.235s
user 0m3.184s
sys 0m0.015s
答案 3 :(得分:4)
Haskell的列表是基于堆的,而你的C代码非常紧张,根本不使用堆。您需要重构以删除对列表的依赖。
答案 4 :(得分:4)
即使我有点迟了,这是我的,我删除了对列表的依赖,这个解决方案也根本不使用堆。
{-# 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
我使用ghc -O2 -fvia-C -optc-O3 -Wall euler.hs
编译了这个部分,并且它在5秒内运行,而开始实现的是80。它不使用Integer
,但因为我使用的是64位计算机,结果可能会被欺骗。
在这种情况下,编译器可以取消装箱所有Int
,从而产生非常快的代码。它的运行速度比我到目前为止看到的所有其他解决方案都要快,但C仍然更快。