性能 – Haskell:为什么Int比Word64更糟糕,为什么我的程序比C慢?

我正在读一篇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.2s.

该文章中给出的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倍,更快速度!

我已经尝试了BangPatterns进行明确的严格评估,但没有显着的性能增益可以被注意到 – 我猜GHC的严格分析是足够聪明的,以处理这样一个简单的情况.

我的问题是:

>为什么Word版本比Int一样更快?
>为什么这个Haskell程序比C中慢?

该程序的性能取决于几个因素.如果我们把它们全部放在正确的位置,那么性能就会和C程序一样.了解这些因素:

1.使用和比较正确的字词大小

发布的C代码片段不完全正确;它在所有架构上使用32位整数,而Haskell Int-s在64位机器上为64位.在任何事情之前,我们应该确保在两个程序中使用相同的字大小.

此外,我们应该总是在我们的Haskell代码中使用本机大小的整数类型.因此,如果我们使用64位系统,我们应该使用64位数字,避免使用Int32-s和Word32-s,除非有特定的需求.这是因为对非本地整数的操作主要实现为foreign calls rather than primops,因此它们显着变慢.

在collat​​z下划分

div比Int更慢,因为div处理负数differently.如果我们使用div并切换到Word,程序会变得更快,因为div与Word相同.与Int作品一样好.然而,这仍然不如C那么快.我们可以通过将位移位到右边来除以二.由于某种原因,即使LLVM也不会在这个例子中减少这种特殊的强度,所以我们最好用手来替换“n”.

检查均匀度

检查最快的方法是检查最低有效位. LLVM甚至可以进行优化,而本机代码不能.所以,如果我们在本地代码中,甚至n可以被替换为n.& 1 == 0,这给了很好的性能提升.

但是,我发现GHC 7.10有一些性能错误.在这里,即使对于这种破坏性能的Word也没有内联(在代码最热的部分中调用一个堆分配的Word框的函数).所以这里我们应该使用rem n 2 == 0或n.& 1 == 0而不是偶数.即使对于Int也是内联的罚款.

将collat​​zLen中的列表进行融合

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

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和n = 10000000,上述程序在2.8秒内运行,而等效的C程序在2.4秒内运行.如果我在没有LLVM的情况下编译相同的代码,那么我会得到12.4秒的运行时.这种放缓完全是因为缺乏对偶的优化.如果我们使用.& 1 == 0,那么减速消失.

5.计算最大长度时,清除列表

甚至没有GHC 7.10可以做到这一点,所以我们必须诉诸手动循环写作.

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和n = 10000000,上述代码在2.1秒内运行,而C程序在2.4秒内运行.如果我们要在没有LLVM和GHC 7.10的情况下实现类似的性能,那么我们只需要手动应用重要的缺失优化:

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和n = 10000000,我们的代码在2.6秒内运行.

翻译自:https://stackoverflow.com/questions/29875886/haskell-why-does-int-performs-worse-than-word64-and-why-my-program-is-far-slow

转载注明原文:性能 – Haskell:为什么Int比Word64更糟糕,为什么我的程序比C慢?