Haskell:为什么Int的性能比Word64差,为什么我的程序远比C慢?

时间:2015-04-26 09:25:58

标签: performance haskell optimization

我正在读一篇how slow Haskell it is in playing with Collatz conjecture的文章,它基本上说明如果你将三个加一个乘以一个奇数,或者将一个偶数除以一个,你最终会得到一个。例如,3 - > 10 - > 5 - > 16 - > 8 - > 4 - > 2 - > 1。

本文中给出的程序是计算给定范围内最长的Collat​​z序列。 C版本是:

#include <stdio.h>

int main(int argc, char **argv) {
   int max_a0 = atoi(argv[1]); 
   int longest = 0, max_len = 0;
   int a0, len;
   unsigned long a;

   for (a0 = 1; a0 <= max_a0; a0++) {
      a = a0;
      len = 0;

      while (a != 1) {
         len++;
         a = ((a%2==0)? a : 3*a+1)/2;
      }

      if (len > max_len) {
         max_len = len;
         longest = a0;
      }
   }
   printf("(%d, %d)\n", max_len, longest);
   return 0;
}

使用Clang O2进行编译,它在我的计算机上运行0.2秒。

该文章中给出的Haskell版本明确地将整个序列生成为列表,然后计算中间列表的长度。它比C版慢10倍。但是,由于作者使用LLVM作为后端,我没有安装,我无法重现这一点。使用GHC 7.8和默认后端,它在我的Mac上运行10秒,比C版本慢50倍。

然后,我使用尾递归编写一个版本,而不是生成中间列表:

collatzNext :: Int -> Int
collatzNext a
  | even a    = a `div` 2
  | otherwise = (3 * a + 1) `div` 2

collatzLen :: Int -> Int
collatzLen n = collatzIter n 0
  where
    collatzIter 1 len = len
    collatzIter n len = collatzIter (collatzNext n) (len + 1)

main = do
  print $ maximum $ [collatzLen x | x <- [1..1000000]]

使用GHC 7.8和O2编译,运行时间为2秒,比C版本慢10倍。

有趣的是,当我将类型注释中的Int更改为Word时,它只花了1秒,快了2倍!

我已经尝试过BangPatterns进行明确的严格评估,但没有注意到显着的性能提升 - 我猜GHC严格的分析足够聪明,可以处理这样一个简单的场景。

我的问题是:

  1. 为什么Word版本与Int版本相比更快?
  2. 为什么这个Haskell程序与C语言相比这么慢?

1 个答案:

答案 0 :(得分:19)

该计划的表现取决于几个因素。如果我们所有这些都正确,那么性能将与C程序的性能相同。经历这些因素:

<强> 1。使用和比较正确的单词大小

发布的C代码段不完全正确;它在所有体系结构上使用32位整数,而Haskell Int32在64位机器上是64位。除此之外,我们应该确保在两个程序中使用相同的字大小。

此外,我们应该始终在Haskell代码中使用原生大小的整数类型。因此,如果我们使用的是64位系统,我们应该使用64位数字,并避开Word32 - s和collatzNext - s,除非特别需要它们。这是因为对非本机整数的操作主要实现为foreign calls rather than primops,因此它们的速度要慢得多。

<强> 2。分为div

quotIntdiv,因为div处理负数differently。如果我们使用Word并切换到div,则程序会变得更快,因为quotWord的{​​{1}}相同。 quotInt的效果一样好。然而,这仍然没有C那么快。我们可以通过向右移位来除以2。由于某些原因,在这个例子中,LLVM甚至没有这种特殊的强度降低,因此我们最好手动完成,将quot n 2替换为shiftR n 1

第3。检查均匀度

检查这一点的最快方法是检查最低有效位。 LLVM可以优化even,而本机代码则不能。因此,如果我们使用的是原生代码,则even n可以替换为n .&. 1 == 0,这样可以提升性能。

然而,我发现GHC 7.10出现了一些性能问题。在这里,我们没有为even获得内联Word,这会破坏性能(在代码的最热部分调用带有堆分配Word框的函数执行此操作)。因此,我们应该使用rem n 2 == 0n .&. 1 == 0代替eveneven的{​​{1}}内联得很好。

<强> 4。在Int

中融合列表

这是一个至关重要的因素。链接的博客文章对此有点过时了。 GHC 7.8不能在这里做融合,但7.10可以。这意味着使用GHC 7.10和LLVM,我们可以方便地获得类似C的性能,而无需显着修改原始代码。

collatzLen

使用collatzNext a = (if even a then a else 3*a+1) `quot` 2 collatzLen a0 = length $ takeWhile (/= 1) $ iterate collatzNext a0 maxColLen n = maximum $ map collatzLen [1..n] main = do [n] <- getArgs print $ maxColLen (read n :: Int) ghc-7.10.1 -O2 -fllvm,上述程序以 2.8 秒运行,而等效的C程序以 2.4 秒运行。如果我在没有LLVM的情况下编译相同的代码,那么我将获得 12.4 第二个运行时。这种放缓完全是因为n = 10000000缺乏优化。如果我们使用even,那么减速就会消失。

<强> 5。在计算最大长度时融合列表

即使GHC 7.10也不能这样做,所以我们不得不求助于手动循环写作。

a .&. 1 == 0

现在,对于collatzNext a = (if a .&. 1 == 0 then a else 3*a+1) `shiftR` 1 collatzLen = length . takeWhile (/= 1) . iterate collatzNext maxCol :: Int -> Int maxCol = go 1 1 where go ml i n | i > n = ml go ml i n = go (max ml (collatzLen i)) (i + 1) n main = do [n] <- getArgs print $ maxCol (read n :: Int) ghc-7.10.1 -O2 -fllvm,上述代码在 2.1 秒内运行,而C程序在 2.4 秒内运行。如果我们想要在没有LLVM和GHC 7.10的情况下实现类似的性能,我们只需手动应用重要的遗漏优化:

n = 10000000

现在,使用collatzLen :: Int -> Int collatzLen = go 0 where go l 1 = l go l n | n .&. 1 == 0 = go (l + 1) (shiftR n 1) | otherwise = go (l + 1) (shiftR (3 * n + 1) 1) maxCol :: Int -> Int maxCol = go 1 1 where go ml i n | i > n = ml go ml i n = go (max ml (collatzLen i)) (i + 1) n main = do [n] <- getArgs print $ maxCol (read n :: Int) ghc-7.8.4 -O2,我们的代码以 2.6 秒运行。

相关问题