【问题标题】:Comparing speed of Haskell and C for the computation of primes比较 Haskell 和 C 计算素数的速度
【发布时间】:2012-08-19 12:59:03
【问题描述】:

我最初写了这个(蛮力和低效的)计算素数的方法,目的是确保在 Haskell 中使用“if-then-else”与使用守卫之间在速度上没有区别(而且没有区别! )。但后来我决定写一个 C 程序来比较,我得到了以下结果(Haskell 慢了 25% 以上):

(请注意,我从以下帖子中得到了使用 rem 而不是 mod 以及编译器调用中的 O3 选项的想法:On improving Haskell's performance compared to C in fibonacci micro-benchmark

Haskell : Forum.hs

divisibleRec :: Int -> Int -> Bool
divisibleRec i j 
  | j == 1         = False 
  | i `rem` j == 0 = True 
  | otherwise      = divisibleRec i (j-1)

divisible::Int -> Bool
divisible i = divisibleRec i (i-1)

r = [ x | x <- [2..200000], divisible x == False]

main :: IO()
main = print(length(r))

C : main.cpp

#include <stdio.h>

bool divisibleRec(int i, int j){
  if(j==1){ return false; }
  else if(i%j==0){ return true; }
  else{ return divisibleRec(i,j-1); }
}

bool divisible(int i){ return divisibleRec(i, i-1); }

int main(void){
  int i, count =0;
  for(i=2; i<200000; ++i){
    if(divisible(i)==false){
      count = count+1;
    }
  }
  printf("number of primes = %d\n",count);
  return 0;
}

我得到的结果如下:

编译时间

time (ghc -O3 -o runProg Forum.hs)
real    0m0.355s
user    0m0.252s
sys 0m0.040s

time (gcc -O3 -o runProg main.cpp)
real    0m0.070s
user    0m0.036s
sys 0m0.008s

以及以下运行时间:

在 Ubuntu 32 位上的运行时间

Haskell
17984
real    0m54.498s
user    0m51.363s
sys 0m0.140s


C++
number of primes = 17984
real    0m41.739s
user    0m39.642s
sys 0m0.080s

Haskell 的运行时间给我留下了深刻的印象。但是我的问题是:我可以做些什么来加快haskell程序的速度吗:

  1. 更改底层算法(很明显,通过更改算法可以获得巨大的加速;但我只是想了解在语言/编译器方面我可以做些什么来提高性能)
  2. 调用 llvm 编译器(因为我没有安装)

[编辑:内存使用情况]

在 Alan 发表评论后,我注意到 C 程序使用恒定数量的内存,而 Haskell 程序的内存大小会缓慢增长。起初我认为这与递归有关,但 gspr 在下面解释了为什么会发生这种情况并提供了解决方案。 Will Ness 提供了一种替代解决方案,该解决方案(如 gspr 的解决方案)也确保内存保持静态。

[编辑:大运行总结]

最大测试数:200,000:

(54.498s/41.739s) = Haskell 慢 30.5%

最大测试数:400,000:

3m31.372s/2m45.076s = 211.37s/165s = Haskell 慢 28.1%

最大测试数:800,000:

14m3.266s/11m6.024s = 843.27s/666.02s = Haskell 慢 26.6%

[编辑:艾伦的代码]

这是我之前写的没有递归的代码,我在 200,000 上测试过:

#include <stdio.h>

bool divisibleRec(int i, int j){
  while(j>0){
    if(j==1){ return false; }
    else if(i%j==0){ return true; }
    else{ j -= 1;}
  }
}


bool divisible(int i){ return divisibleRec(i, i-1); }

int main(void){
  int i, count =0;
  for(i=2; i<8000000; ++i){
    if(divisible(i)==false){
      count = count+1;
    }
  }
  printf("number of primes = %d\n",count);
  return 0;
}

有和没有递归的C代码结果如下(800,000):

使用递归:11m6.024s

没有递归:11m5.328s

请注意,无论最大数量如何,可执行文件似乎都占用了 60kb(如系统监视器中所示),因此我怀疑编译器正在检测此递归。

【问题讨论】:

  • 使用r = filter (not . divisible) [2..200000] 代替列表理解有什么不同吗?
  • haskell.org/haskellwiki/Prime_numbers 可能很有趣,也许有一些有趣的东西(比如数组)。
  • @dbaupp : 不使用 "r = filter (not . divisible) [2..200000]" 没有区别。谢谢
  • 我不知道 Haskell 是如何在内部实现的,但是在 C 中这种类型的超深度递归被认为是一种不好的做法,我很惊讶这并没有堆栈溢出和崩溃。由于它不会崩溃,优化器可能会识别出不必要的递归并将其删除。如果它实际上递归的深度比你可能是缓存绑定而不是计算绑定。将 C 版本中的递归调用转换为简单的嵌套 for 循环将是更典型的 C。
  • @artella:你的意思是当你增加200000这个数字时,它所占用的内存量会增加?这并不奇怪:当这个数字增加时,列表r 也会增加。您的代码最后需要所有 r 来计算其长度。另一方面,C 代码只是增加一个计数器。如果你想要恒定的内存使用,你也必须在 Haskell 中做类似的事情(代码仍然非常 Haskelly)。尝试main = print $ foo [2..200000]foo (x:xs) = if divisible x then foo xs else 1 + foo xs(您还需要基本情况​​foo [] = 0

标签: c math optimization haskell ghc


【解决方案1】:

这并不是真正回答您的问题,而是您在评论中提出的关于当 200000 增长时内存使用量增加的评论。

当这个数字增加时,列表r 也会增加。您的代码最后需要所有 r 来计算其长度。另一方面,C 代码只是增加一个计数器。如果你想要持续使用内存,你也必须在 Haskell 中做类似的事情。代码仍然非常Haskelly,总的来说这是一个明智的提议:你并不需要divisibleFalse 的数字列表,你只需要知道有多少。

你可以试试

main :: IO ()
main = print $ foldl' (\s x -> if divisible x then s else s+1) 0 [2..200000]

foldl'Data.List 的一个更严格的 foldl,可避免建立 thunk)。

【讨论】:

  • 啊谢谢!我完全错过了。我非常专注于递归,以至于我认为递归是内存增加的原因!我试过你的代码,内存确实保持不变。干杯。
  • 列表r 根本不应该增长。它应该在构建时被消耗掉,消耗掉的东西会立即被丢弃。我尝试用 r3 n = length [ () | x &lt;- [2..n], divisible x == False] 替换 OP 的代码,但没有任何区别。
  • @WillNess :对于我最初编写的程序,内存在 ubuntu 中确实增加了(而且根据您的建议,它似乎也增加了)。但是对于 gspr 的上述建议,内存使用量保持不变。谢谢
  • @artella 最后一件让我烦恼的事,就是记忆。根据 Thomas M. DuBuisson 在帖子末尾的评论,这是关于在名称 r 下拥有一个 named list。我的建议是使用 function r3 n = length ...。你说它在内存使用中不是恒定的,这对我来说仍然很奇怪。也许您测试了我的代码行,但仍然使用命名列表?也许这是一个误会?你能用一个函数测试它吗?给事物命名通常会导致它们 2 保留在内存中,但 r3 描述的计算 应该 在常量内存中运行..?
  • @WillNess:我很抱歉,你是绝对正确的。这次我看了更长的时间,这就是我注意到的:最初它增加到 800KB,然后突然下降到 500KB。然后它再次开始增加,直到达到 744KB,之后保持在这个水平。谢谢。
【解决方案2】:

好棒的模式给你一个很小的胜利(就像 llvm 一样,但你似乎已经预料到了):

{-# LANUGAGE BangPatterns #-}

divisibleRec !i !j | j == 1         = False

在我的 x86-64 上,我通过切换到较小的表示形式(例如 Word32)获得了巨大的胜利:

divisibleRec :: Word32 -> Word32 -> Bool
...
divisible :: Word32 -> Bool

我的时间安排:

$ time ./so             -- Int
2262

real    0m2.332s

$ time ./so              -- Word32
2262

real    0m1.424s

这更接近于您的 C 程序,它仅使用 int。它仍然与性能不匹配,我怀疑我们必须查看核心才能找出原因。

编辑:正如我已经注意到的,内存使用是关于命名列表r。我只是内联了r,让它为每个不可分割的值输出一个1 并取和:

main = print $ sum $ [ 1 | x <- [2..800000], not (divisible x) ]

【讨论】:

  • length [ () | x &lt;- [2..n], divisible x == False],这对我来说没什么区别。 length [()|x&lt;-[2..n],and [rem x d&gt;0|d&lt;-[x-1,x-2..2]]] 慢得多,使用 all 更慢。
  • 嗨,“BangPatterns”divisibleRec 的其余代码是什么?谢谢
  • 代码呢?我没有更改任何我没有显示的代码(所以我只是添加了 bang 模式并修改了类型签名,然后调整了 main 以修复内存使用。
  • @ThomasM.DuBuisson :抱歉,我不确定 BangPatterns 是什么。我试过了,它有效,但它对速度没有影响。谢谢
  • 主要区别在于不同的类型 - 这对您有什么帮助吗?另外,你的平台是什么?
【解决方案3】:

另一种写下算法的方法是

main = print $ length [()|x<-[2..200000], and [rem x d>0|d<-[x-1,x-2..2]]]

不幸的是,它运行速度较慢。使用all ((&gt;0).rem x) [x-1,x-2..2] 作为测试,它的运行速度仍然较慢。但也许你会在你的设置上测试它。

用带有爆炸模式的显式循环替换您的代码没有任何区别:

{-# OPTIONS_GHC -XBangPatterns #-}
r4::Int->Int
r4 n = go 0 2 where
  go !c i | i>n = c
          | True = go (if not(divisible i) then (c+1) else c) (i+1)

divisibleRec::Int->Int->Bool
divisibleRec i !j | j == 1 = False 
                  | i `rem` j == 0 = True 
                  | otherwise = divisibleRec i (j-1)

【讨论】:

  • 嗨,是的,不幸的是它比较慢。与原始代码的 1 分钟相比,它花费了 9 分钟 55.231 秒。但是它非常简洁,这很好:)
  • divisibleRec 中的爆炸模式完全没有必要:代码所做的第一件事就是比较j==1,这会强制j 无论如何...)。
  • 非常感谢。我不确定它是什么,所以尝试了一下,是的,它在 ubuntu 中也没有什么区别。谢谢
【解决方案4】:

当我开始使用 Haskell 编程时,我也对它的速度印象深刻。您可能有兴趣阅读article 的第 5 点“Haskell 的速度”。

【讨论】:

  • 是的,速度方面很好,但我对内存逐渐增加的事实印象不深。必须有一种方法来实现算法并让程序使用恒定数量的内存(如 C 程序)....但我还没有找到它。
  • 见 gspr 的评论。他在上面的帖子解释了为什么内存在增加并让我放心!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-10-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-02-20
  • 2020-05-10
  • 1970-01-01
相关资源
最近更新 更多