【发布时间】: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程序的速度吗:
- 更改底层算法(很明显,通过更改算法可以获得巨大的加速;但我只是想了解在语言/编译器方面我可以做些什么来提高性能)
- 调用 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