关于C#:如何提高这个Haskell程序的性能?

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版本)。


为了测试目的,我刚刚设置了searchTo = 100000。所花费的时间是7.34s。一些修改会导致一些大的改进:

  • Integer代替Int64。这将时间提高到1.75秒。

  • 使用一个累加器(你不需要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
  • 使用quotRem重写nextNumber,从而避免计算除法两次(一次在even中,一次在div中)。1.27秒。

    1
    2
    3
    4
    5
    nextNumber :: Integer -> Integer
    nextNumber n
        | r == 0    = q
        | otherwise = 6*q + 4
        where (q,r) = quotRem n 2

  • 使用Schwartzian变换代替maximumBymaximumBy . comparing的问题是,对每个值多次调用sequenceLength函数。0.32秒。

    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系统,它们显示出一些不同的特性:使用Integer而不是Int64并不会像使用32位GHC那样提高性能,在这种情况下,每一个Int64操作都会导致C调用的开销,而使用Integer的计算不需要在有符号32位整数中进行前置运算。ign call(因为只有很少的操作超过这个范围,所以在32位GHC上,Integer是更好的选择)。

    • C:0.3秒
    • 原haskell:14.24秒,用Integer代替Int6433.96秒
    • KennyTM改进版:5.55秒,使用Int1.85秒
    • Chris Kuklewicz的版本:5.73秒,使用Int:1.90秒
    • fuzzxl版本:3.56秒,使用quotRem而不是divMod:1.79秒

    那我们有什么?

  • 用累加器计算长度,以便编译器可以(基本上)将其转换为循环
  • 不重新计算比较的序列长度
  • 不要使用divresp。divMod当不需要时,quot响应。quotRem快得多
  • 还缺少什么?

    1
    2
    3
    4
    if (j % 2 == 0)
        j = j / 2;
    else
        j = 3 * j + 1;

    我使用的任何C编译器都将测试j % 2 == 0转换为位屏蔽,并且不使用除法指令。GHC还没有这样做。因此,测试even n或计算n `quotRem` 2是相当昂贵的操作。在KennyTM的Integer版本中,将nextNumber替换为

    1
    2
    3
    4
    nextNumber :: Integer -> Integer
    nextNumber n
        | fromInteger n .&. 1 == (0 :: Int) = n `quot` 2
        | otherwise = 3*n+1

    将运行时间缩短到3.25秒(注:对于Integern `quot` 2n `shiftR` 1快,需要12.69秒!).

    Int版本中做同样的操作可以将运行时间缩短到0.41秒。对于Ints,除以2的位移比quot操作快一点,将运行时间缩短到0.39秒。

    消除了列表的结构(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。


    比较可能是重新计算sequenceLength太多。这是我最好的版本:

    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慢,但它使用任意精度整数(通过Integer类型):

    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

    我用ghc -O2 -fvia-C -optc-O3 -Wall euler.hs编译了这篇文章,它在5秒内运行,而在开始的实现中只有80秒。它不使用Integer,但是因为我在64位机器上,结果可能会被欺骗。

    在这种情况下,编译器可以取消对所有Int的装箱,从而产生非常快的代码。它比我迄今为止看到的所有其他解决方案都运行得更快,但C仍然更快。


    Haskell的列表是基于堆的,而您的C代码非常紧凑,根本不使用堆。您需要重构以删除对列表的依赖关系。