关于性能:c与haskell collatz推测速度比较

C vs Haskell Collatz conjecture speed comparison

我第一次真正的编程经验是在Haskell。为了满足我的特殊需要,我需要一个易于学习、易于编码和易于维护的工具,我可以说它很好地完成了任务。

然而,在某一时刻,我的任务规模变得更大了,我认为C可能更适合他们,它确实做到了。也许我在编程方面不够熟练,但我没能使haskell像c一样快速高效,尽管我听说适当的haskell有类似的性能。

最近,我想我会再次尝试一些haskell,虽然它对于一般的简单(在计算方面)任务仍然很好,但它似乎无法将C的速度与collatz猜想等问题匹配起来。我读过:

与项目euler的速度比较:c与python与erlang与haskell

GHC优化:collatz猜想

使用haskell的collatz列表实现

但从我看来,简单的优化方法包括:

  • 选择"更紧"的类型,如int64而不是integer
  • 打开GHC优化
  • 使用简单的优化技术,如避免不必要的计算或更简单的函数

对于真正的大数字来说,仍然不能使haskell代码接近几乎相同的(在方法论方面)C代码。唯一能使它的性能与C的(对于大规模问题)相媲美的是使用优化方法,使代码成为一个长的、可怕的单态地狱,这违背了haskell(和i)非常重视的原则。

这是C版:

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
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

int32_t col(int64_t n);

int main(int argc, char **argv)
{
    int64_t n = atoi(argv[1]), i;
    int32_t s, max;

    for(i = 2, max = 0; i <= n; ++i)
    {
        s = col(i);
        if(s > max) max = s;
    }
    printf("%d
"
, max);

    return 0;
}

int32_t col(int64_t n)
{
    int32_t s;

    for(s = 0; ; ++s)
    {
        if(n == 1) break;
        n = n % 2 ? 3 * n + 1 : n / 2;
    }

    return s;
}

哈斯克尔版本:

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
module Main where

import System.Environment (getArgs)
import Data.Int (Int32, Int64)

main :: IO ()
main = do
    arg <- getArgs
    print $ maxCol 0 (read (head arg) :: Int64)

col :: Int64 -> Int32
col x = col' x 0

col' :: Int64 -> Int32 -> Int32
col' 1 n            = n
col' x n
    | rem x 2 == 0  = col' (quot x 2) (n + 1)
    | otherwise     = col' (3 * x + 1) (n + 1)

maxCol :: Int32 -> Int64 -> Int32
maxCol maxS 2   = maxS
maxCol maxS n
    | s > maxS  = maxCol s (n - 1)
    | otherwise = maxCol maxS (n - 1)
    where s = col n

tl;dr:haskell代码的编写速度快,维护简单,仅用于计算简单的任务,在性能至关重要时会失去这一特性吗?


haskell代码的最大问题是您正在划分,而在C版本中您没有划分。

是的,您编写了n % 2n / 2,但是编译器用移位和按位和替换了它。不幸的是,GHC还没有被教导要这么做。

如果你自己做替换

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
module Main where

import System.Environment (getArgs)
import Data.Int (Int32, Int64)
import Data.Bits

main :: IO ()
main = do
    arg <- getArgs
    print $ maxCol 0 (read (head arg) :: Int64)

col :: Int64 -> Int32
col x = col' x 0

col' :: Int64 -> Int32 -> Int32
col' 1 n            = n
col' x n
    | x .&. 1 == 0  = col' (x `shiftR` 1) (n + 1)
    | otherwise     = col' (3 * x + 1) (n + 1)

maxCol :: Int32 -> Int64 -> Int32
maxCol maxS 2   = maxS
maxCol maxS n
    | s > maxS  = maxCol s (n - 1)
    | otherwise = maxCol maxS (n - 1)
    where s = col n

使用64位GHC,您可以获得类似的速度(0.35s与C的0.32s在我的盒子上,限制为1000000)。如果您使用llvm后端进行编译,甚至不需要用逐位操作替换% 2/ 2,llvm会为您这样做(但它会为您的原始haskell源生成较慢的代码0.4s,令人惊讶的是,通常情况下,llvm在循环优化时并不比本地代码生成器差)。

有了32位GHC,您将无法获得可比的速度,因为有了这些,64位整数上的原始操作是通过C调用实现的——对于32位系统上的64位快速操作,没有足够的需求让它们作为primops实现;在GHC上工作的少数人花费时间做其他更重要的事情。

TL;DR: Is Haskell code quick to write and simple to maintain only for computationally simple tasks and loses this characteristic when performance is crucial?

那要视情况而定。您必须了解GHC从什么类型的输入中生成什么类型的代码,并且必须避免一些性能陷阱。经过一点实践,很容易达到gcc-o3的2倍速度。