比较 Haskell 和 C 计算素数的速度

Comparing speed of Haskell and C for the computation of primes

提问人:artella 提问时间:8/19/2012 最后编辑:Communityartella 更新时间:8/20/2012 访问量:1713

问:

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

(请注意,我从以下帖子中得到了使用 rem 而不是 mod 的想法,以及编译器调用中的 O3 选项:关于在斐波那契微基准测试中与 C 相比提高 Haskell 的性能)

Haskell:论坛.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:

3 分 31.372 秒/2 分 45.076 秒 = 211.37 秒/165 秒 = Haskell 慢 28.1%

最大测试数量 : 800,000:

14 分 3.266 秒/11 分 6.024 秒 = 843.27 秒/666.02 秒 = 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):

递归:11分6.024秒

无递归:11分5.328秒

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

C 数学 优化 Haskell GHC

评论

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

答:

1赞 Will Ness 8/19/2012 #1

写下算法的另一种方法是

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

不幸的是,它运行速度较慢。用作测试,它的运行速度仍然较慢。但也许你会在你的设置上测试它。all ((>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)

评论

0赞 artella 8/20/2012
嗨,是的,不幸的是它比较慢。它花了 9 分 55.231 秒,而原始代码需要 1 分钟。但是它非常简洁,这很好:)
1赞 Will Ness 8/20/2012
(完全没有必要的爆炸模式:代码做的第一件事就是比较,无论如何都会强制......divisibleRecj==1j
0赞 artella 8/20/2012
很酷,谢谢。我不确定它是什么,所以尝试了一下,是的,它在 ubuntu 中也没有区别。谢谢
0赞 Claudi 8/20/2012 #2

当我开始用Haskell编程时,它的速度也给我留下了深刻的印象。您可能有兴趣阅读本文的第 5 点“Haskell 的速度”。

评论

0赞 artella 8/20/2012
是的,速度方面很好,但我对内存逐渐增加的事实印象深刻。必须有一种方法来实现算法并让程序使用恒定的内存量(如 C 程序)......但我还没有找到它。
0赞 artella 8/20/2012
参见 GSPR 的评论。他上面的帖子解释了为什么记忆力在增加,让我放心!
2赞 Thomas M. DuBuisson 8/20/2012 #3

好吧,bang patters 给你带来了一个非常小的胜利(就像 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

编辑:正如我已经指出的那样,内存使用是关于命名列表的。我只是内联,让它为每个不可分割值输出一个并取总和:rr1

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

评论

0赞 Will Ness 8/20/2012
或者,这对我来说没有区别。 速度要慢得多,使用速度也更慢。length [ () | x <- [2..n], divisible x == False]length [()|x<-[2..n],and [rem x d>0|d<-[x-1,x-2..2]]]all
0赞 artella 8/20/2012
嗨,“BangPatterns”divisibleRec 的其余代码是什么?谢谢
0赞 Thomas M. DuBuisson 8/20/2012
代码呢?我没有更改任何我没有显示的代码(所以我只是添加了爆炸模式并修改了类型签名,然后调整了 main 以修复内存使用。
0赞 artella 8/20/2012
@ThomasM.DuBuisson : 对不起,我不确定BangPatterns是什么。我试了一下,它起作用了,但它对速度没有影响。谢谢
0赞 Thomas M. DuBuisson 8/20/2012
主要区别在于不同的类型 - 这对你有什么作用吗?另外,你的平台是什么?
5赞 gspr 8/20/2012 #4

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

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

您可以尝试

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

(foldl'是更严格的,避免了 thunks 的建立)。foldlData.List

评论

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