14

我遇到了这个问题,它比较了各种编译器以天真的方式计算斐波那契数的性能。

我尝试用 Haskell 来做这件事,看看它与 C 相比如何。

C代码:

#include <stdio.h>
#include <stdlib.h>

int fib (int n) {
  if (n < 2) return 1;
  return fib (n-1) + fib (n-2);
}

int main (int argc, char* argv[]) {
  printf ("%i\n", fib (atoi(argv[1])));
  return 0;
}

结果:

> gcc -O3 main.c -o fib
> time ./fib 40
165580141
real    0m0.421s
user    0m0.420s
sys 0m0.000s

哈斯克尔:

module Main where
import System.Environment (getArgs)

fib :: Int -> Int
fib n | n < 2 = 1
      | otherwise = fib (n-1) + fib (n-2)

main = getArgs >>= print . fib . read . head

结果:

> ghc -O3 -fllvm -optlo-O3 Main.hs -o fib
> time ./fib 40
165580141
real    0m1.476s
user    0m1.476s
sys 0m0.000s

分析与

> ghc -O3 -fllvm -optlo-O3 -prof -auto-all -caf-all -rtsopts Main.hs -fforce-recomp -o fib
> ./fib 40 +RTS -prof

表明这fib需要 100% 的时间和分配,这不足为奇。我对堆进行了一些配置,但不知道它们意味着什么:

> ./fib 40 +RTS -hc

在此处输入图像描述

> ./fib 40 +RTS -hd

在此处输入图像描述

所以我的问题是:我能做些什么来让这个 Haskell 程序的性能更接近 C,或者这只是 GHC 在这个微基准测试中碰巧让它变慢的方式吗?(我不是在要求一种渐近更快的算法来计算 fibs。)

非常感谢。

[编辑]

事实证明,这ghc -O3ghc -O3 -fllvm -optlo-O3这种情况下要快。但optlo-block-placement对 LLVM 后端产生了明显的不同:

> ghc -O3 Main.hs -o fib -fforce-recomp
> time ./fib 40
165580141
real    0m1.283s
user    0m1.284s
sys 0m0.000s

> ghc -O3 -fllvm -optlo-O3 -o fib -fforce-recomp
> time ./fib 40
165580141
real    0m1.449s
user    0m1.448s
sys 0m0.000s

> ghc -O3 -fllvm -optlo-O3 -optlo-block-placement -o fib -fforce-recomp
> time ./fib 40
165580141
real    0m1.112s
user    0m1.096s
sys 0m0.016s

我想研究这个的原因是因为对于这个程序,C 和 OCaml 都比 Haskell 快得多。我有点不能接受,想了解更多,以确保我已经做了我能做的一切:D

> ocamlopt main.ml -o fib
> time ./fib 40
165580141
real    0m0.668s
user    0m0.660s
sys 0m0.008s
4

4 回答 4

9

堆配置文件在这里不是很有趣,因为 GHC 编译fib成一个在堆栈上单独操作的函数。只需查看配置文件...仅分配了 800 个字节,这是您main实现的小开销。

就 GHC 的核心级别而言,这实际上得到了尽可能优化。不过,低级代码生成是另一回事。让我们快速深入了解 GHC 生成的代码:

_Main_zdwfib_info:
.Lc1RK:
    leal -8(%ebp),%eax
    cmpl 84(%ebx),%eax
    jb .Lc1RM

这是对堆栈空间的检查。可能是 C 不需要的东西,因为它可以让操作系统处理堆栈空间分配。Haskell 有用户级线程,所以栈空间是手动管理的。

    cmpl $2,0(%ebp)
    jl .Lc1RO

与您的代码中的 2 进行比较。

    movl 0(%ebp),%eax
    decl %eax

参数从堆栈中重新加载并递减以获取递归调用的参数。重新加载可能是不必要的 - 但不确定它是否有所作为。

    movl %eax,-8(%ebp)
    movl $_s1Qp_info,-4(%ebp)
    addl $-8,%ebp
    jmp _Main_zdwfib_info

参数和返回地址被压入栈顶,我们直接跳转到标签处进行递归。

.Lc1RO:
    movl $1,%esi
    addl $4,%ebp
    jmp *0(%ebp)

参数小于2的情况的代码。返回值在寄存器中传递。

底线:一切似乎都在按应有的方式工作,您不太可能通过更改程序来从中榨取更多。自定义堆栈检查是一个明显的减速源,但不确定它是否可以归咎于完整的时间差异。

于 2011-07-16T11:07:04.657 回答
6

正如所说,这些似乎是一个非常微弱的“基准” barsoap。假设我比较以下几乎同样幼稚的程序:

module Main where
import System.Environment (getArgs)

fib ::  Int ->  Int
fib 0 = 1
fib 1 = 1
fib 2 = 2 
fib n = (\x y -> x + y + y )  (fib (n-3))  (fib (n-2) )

main = getArgs >>= print . fib . read . head  

...在另一个角落...

#include <stdio.h>
#include <stdlib.h>

int fib (int n) {
  if (n < 2) return 1;
  if (n < 3) return n;
  return (fib (n-3) + fib (n-2)) + fib (n-2);
}

int main (int argc, char* argv[]) {
  printf ("%i\n", fib (atoi(argv[1])));
  return 0;
}

然后 Gloriousghc粉碎gcc了,毫不奇怪,真的:

$ ghc --make -fforce-recomp fib.hs -o fibh
[1 of 1] Compiling Main             ( fib.hs, fib.o )
Linking fibh ...
$ time ./fibh 40
165580141

real    0m0.013s
user    0m0.007s
sys 0m0.003s

$ gcc fib.c -o fibc
$ time ./fibc 40
165580141

real    0m1.495s
user    0m1.483s
sys 0m0.003s

现在打开优化,ghc加快一点速度:

$ ghc --make -fforce-recomp fib.hs -O3 -o fibhO
$ time ./fibhO 40
165580141

real    0m0.007s
user    0m0.002s
sys 0m0.004s

gcc最终得到了一个线索。

$ gcc fib.c -O3 -o fibcO
$ time ./fibcO 40
165580141

real    0m0.007s
user    0m0.004s
sys 0m0.002s

我认为解释是ghc对常见子表达式消除的谨慎:在“(几乎)一切都是表达式”的情况下是危险的,并且它表明程序员知道如何使用 lambda。

于 2011-07-22T20:26:19.590 回答
3

GHC 编译得很好。下一步是对 GHC 后端的输出进行微优化。在这里使用各种 LLVM 标志会有所帮助。

为此,请使用 ghc-core 检查程序集,并尝试使用 LLVM 的其他标志来查看您得到的结果。

另一种方法是添加少量并行性。

于 2011-07-16T08:56:54.047 回答
1

尝试这个:

fibs :: [Integer]
fibs = 0:1:zipWith (+) fibs (tail fibs)

fib n = fibs !! n

$ time ./fib 10000
  33644[...]6875
  ./fib 10000  0.03s user 0.01s system 89% cpu 0.039 total

(那是在一个好的 ole Athlon64 3200+ 上)

您正在使用的版本对每个n计算fib (n-1)fib (n-2),也就是说,具有大致三角形性质的复杂性。上面的版本是线性的:每个 fib 只计算一次。尽管非 Haskell 编程头脑似乎在想什么,Haskell 不会自动记忆(无论如何,这通常比好的 ole 动态编程要慢)。

Haskell Wiki上有更快(使用数学技巧)版本的 fibonnaci 。

将 C 版本更改为非递归版本,我敢打赌,您会看到 Haskell 和 C 具有非常相似的性能。紧密循环更容易优化。

于 2011-07-16T11:04:13.327 回答