如何提高Haskell程序的性能?
作为学习Haskell的一种方式,我正在解决Project Euler中的问题,并且发现即使在编译时,我的程序也比可比较的C版本慢很多。 我能做些什么来加快我的Haskell程序?
例如,我对问题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("%dn", longest);
return 0;
}
我究竟做错了什么? 或者我天真地认为Haskell甚至可以接近C的速度?
(我使用gcc -O2编译C版本,使用ghc -make -O编译Haskell版本)。
为了测试目的,我只设置searchTo = 100000
。 所花费的时间是7.34s 。 一些修改导致了一些重大改进:
使用Integer
而不是Int64
。 这将时间缩短到1.75秒 。
使用累加器(你不需要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变换而不是maximumBy
。 maximumBy . comparing
的问题maximumBy . comparing
maximumBy . comparing
而言, sequenceLength
函数对于每个值都被调用多次。 0.32s 。
longestSequence = snd $ maximum [(sequenceLength a, a) | a <- [1..searchTo]]
注意:
ghc -O
检查时间,并使用+RTS -s
运行) gcc -O3 -m32
编译的。 尽管这已经很老了,让我来参加,但还有一个关键问题没有得到解决。
首先,我的箱子上的不同节目的时间安排。 由于我在64位Linux系统上,它们显示出不同的特征:使用Integer
而不是Int64
不会像使用32位GHC那样提高性能,其中每个Int64
操作都会产生C调用的代价而Integer
适用于带符号32位整数的计算不需要外部调用(因为在这里只有少数操作超过此范围, Integer
在32位GHC上是更好的选择)。
Integer
而不是Int64
:33.96秒 Int
:1.85秒 Int
:1.90秒 quotRem
而不是divMod
:1.79秒 那么我们有什么?
div
resp。 divMod
当它不是必要的, quot
resp。 quotRem
要快得多 还有什么遗漏?
if (j % 2 == 0)
j = j / 2;
else
j = 3 * j + 1;
我使用的任何C编译器都将测试j % 2 == 0
转换为位掩码,并且不使用分割指令。 GHC还没有(那)那样做。 因此,测试even n
或计算n `quotRem` 2
是相当昂贵的操作。 用nextNumber
Integer
版本替换nextNumber
nextNumber :: Integer -> Integer
nextNumber n
| fromInteger n .&. 1 == (0 :: Int) = n `quot` 2
| otherwise = 3*n+1
将其运行时间减少到3.25秒(注意:对于Integer
, n `quot` 2
n `shiftR` 1
比n `shiftR` 1
快,这需要12.69秒!)。
在Int
版本中执行相同操作可将运行时间缩短到0.41秒。 对于Int
,除2的比特移位比quot
运算quot
快一点,将其运行时间减少到0.39秒。
消除列表的构建(即不在C版本中出现),
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版本中不存在,
if (this_terms > terms)
{
terms = this_terms;
longest = i;
}
出现在内部循环中。 在C版本中取消内循环将其运行时间减少到0.27秒,使得系数为1.4。
比较可能是重新计算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
链接地址: http://www.djcxy.com/p/24345.html
上一篇: How to improve the performance of this Haskell program?
下一篇: Hibernate/Gorm query with distinct results and JOIN on non primary key