【问题标题】:Can I speed up this Haskell algorithm?我可以加速这个 Haskell 算法吗?
【发布时间】:2012-10-04 15:18:46
【问题描述】:

我有这个 haskell 文件,用 ghc -O2 (ghc 7.4.1) 编译,在我的机器上需要 1.65 秒

import Data.Bits
main = do
    print $ length $ filter (\i -> i .&. (shift 1 (i `mod` 4)) /= 0) [0..123456789]

在 C 中使用 gcc -O2 (gcc 4.6.3) 编译的相同算法在 0.18 秒内运行。

#include <stdio.h>
void main() {
    int count = 0;
    const int max = 123456789;
    int i;
    for (i = 0; i < max; ++i)
        if ((i & (1 << i % 4)) != 0)
            ++count;
    printf("count: %d\n", count);
}

更新 我认为这可能是Data.Bits 的东西变慢了,但令人惊讶的是,如果我取消变速并直接执行mod,它实际上运行更慢 5.6 秒!?!

import Data.Bits
main = do
    print $ length $ filter (\i -> (i `mod` 4) /= 0) [0..123456789]

而等效的 C 以 0.16 秒的速度运行稍快:

#include <stdio.h>
void main() {
    int count = 0;
    const int max = 123456789;
    int i;
    for (i = 0; i < max; ++i)
        if ((i % 4) != 0)
            ++count;
    printf("count: %d\n", count);
}

【问题讨论】:

  • Haskell 等效于 %rem,而不是 mod。后者对负数有额外的检查,见stackoverflow.com/questions/339719
  • [0..123456789] 替换为([0..123456789] :: [Int]) 有帮助吗?只是想知道你是否得到了一些不幸的Num 实例推断(例如Integer)。
  • FWIW,使用-O2 -fllvm -funfolding-use-threshold1000 编译让我运行得很快(与 C 差不多)。 (编辑:实际上不是,只是“更快”——我在两者之间进行了一些其他更改,使其与 C 一样快。)
  • 这段代码看起来可以融合成一个循环,但是当我查看核心输出时,filter的调用生成了一个列表。这可能就是 ghc 较慢的原因。
  • @Heatsink:这不会熔断,因为length is not a "good consumer"

标签: haskell


【解决方案1】:

这两段代码做了非常不同的事情。

import Data.Bits
main = do
    print $ length $ filter (\i -> i .&. (shift 1 (i `mod` 4)) /= 0) [0..123456789]

创建一个 123456790 Integer 的列表(懒惰地),取每个余数模 4(首先检查 Integer 是否小到足以包装原始机器整数,然后在除法之后进行符号检查,因为mod 仅返回非负结果 - 尽管在 ghc-7.6.1 中,有一个 primop,所以使用mod 并没有像以前那样刹车之前),将Integer 1 向左移动适当数量的位,这涉及到“大”Integers 的转换和对 GMP 的调用,按位和i - 又一次调用 GMP - 和检查结果是否为 0,这会导致再次调用 GMP 或转换为小整数,不确定 GHC 在这里做了什么。然后,如果结果为非零,则创建一个新的列表单元格,将 Integer 放入其中,并由 length 使用。已经完成了很多工作,其中大部分工作由于未指定的数字类型默认为Integer而变得不必要地复杂。

C 代码

#include <stdio.h>
int main(void) {
    int count = 0;
    const int max = 123456789;
    int i;
    for (i = 0; i < max; ++i)
        if ((i & (1 << i % 4)) != 0)
            ++count;
    printf("count: %d\n", count);
    return 0;
}

(我冒昧地修复了main 的返回类型),做得少得多。它需要一个 int,将其与另一个比较,如果更小,则使用 3(1) 对第一个 int 进行按位和移动,移动 int 1向左适当的位数,按位和第一个int,如果非零增加另一个int,则增加第一个。这些都是机器操作,在原始机器类型上工作。

如果我们将该代码翻译成 Haskell,

module Main (main) where

import Data.Bits

maxNum :: Int
maxNum = 123456789

loop :: Int -> Int -> Int
loop acc i
    | i < maxNum = loop (if i .&. (1 `shiftL` (i .&. 3)) /= 0 then acc + 1 else acc) (i+1)
    | otherwise  = acc

main :: IO ()
main = print $ loop 0 0

我们得到了更接近的结果:

C, gcc -O3:
count: 30864196

real    0m0.180s
user    0m0.178s
sys     0m0.001s

Haskell, ghc -O2:
30864196

real    0m0.247s
user    0m0.243s
sys     0m0.003s

Haskell, ghc -O2 -fllvm:
30864196

real    0m0.144s
user    0m0.140s
sys     0m0.003s

GHC 的本机代码生成器并不是一个特别好的循环优化器,因此在这里使用 llvm 后端会有很大的不同,但即使是本机代码生成器也不会做得太差。

好的,我已经完成了将模数计算替换为按位和手动的二次幂模数的优化,GHC 的本机代码生成器还没有这样做,所以使用 ```rem@ 987654342@.&. 3`,本机代码生成器生成的代码(此处)需要 1.42 秒才能运行,但 llvm 后端会进行优化,并生成与手工优化相同的代码。

现在,让我们转向gspr's question

虽然 LLVM 对原始代码没有太大影响,但对修改后的代码确实有影响(我很想知道为什么...)。

好吧,原来的代码使用了Integers 和 列表,llvm 不太清楚如何处理这些,它无法将该代码转换为循环。修改后的代码使用Ints,vector 包将代码重写为循环,所以 llvm 确实知道如何优化它,这表明了。

(1) 假设一台普通的二进制计算机。这种优化是由普通 C 编译器完成的,即使没有任何优化标志,除非在非常罕见的平台上,div 指令比移位更快。

【讨论】:

  • 感谢您尝试我的问题(并写了一个很棒而冗长的答案!),但我必须承认我撒了谎:当我说“原始代码”时,我实际上是指“带有 @ 的原始代码” 987654347@".
  • 您的意思是“具有某些类型签名的原始代码,强制所有内容为Int”?然后关于列表的观点仍然适用,llvm 不是为此而设计的,因此它不是特别擅长将列表代码转换为循环。 vector 包是,但是,如果你做你的“listish”代码是正确的。
  • 太棒了!那么现在......如何使 C 代码的执行速度与 Haskell 一样快? :)
  • 手写循环并不总是必要的,您通常可以使用像vector's 这样的融合框架来实现相同的目的。哪个更容易和/或提供更好的性能取决于您的习惯(我很少使用Vectors,我习惯于编写循环,所以如果您习惯于使用@987654352,我通常会发现这更容易@s,你会发现这更容易),以及计算的结构(你总是可以摆弄手写循环来尝试挤出更多,但如果你让融合框架编写循环,你就会被束缚以前做过什么,但要击败一个好的框架并不容易)。
  • 在可以使用它们的地方,IntWord 提供最佳性能,因此请尽可能使用它们并且速度很重要。严谨和懒惰很重要。在无论如何都需要评估参数的地方,让它变得严格,在你可以开始产生结果的地方,让它保持懒惰。 GHC 还没有做很多低级优化,所以要注意用比特玩弄技巧来替代其他操作(主要是除法)。避免使用divmod,其中quotrem 会做正确的事情。 LLVM 是否优于原生代码生成器取决于,但 NCG 明显更快的情况很少见。
【解决方案2】:

很少有东西能胜过带有严格累加器的手写循环:

{-# LANGUAGE BangPatterns #-}

import Data.Bits

f :: Int -> Int
f n = g 0 0
  where g !i !s | i <= n    = g (i+1) (if i .&. (unsafeShiftL 1 (i `rem` 4)) /= 0 then s+1 else s)
                | otherwise = s

main = print $ f 123456789

除了到目前为止提到的技巧之外,这还将shift 替换为unsafeShiftL,它不会检查其参数。

使用-O2-fllvm 编译,这比我机器上的原版快13 倍左右。

注意:测试x的位i是否设置可以更清楚地写为x `testBit` i。这将产生与上述相同的程序集。

【讨论】:

    【解决方案3】:

    向量而不是列表,折叠而不是过滤器和长度

    将列表替换为unboxed vector 并将过滤器和长度替换为折叠(即递增计数器)显着改善了我的时间。这是我使用的:

    import qualified Data.Vector.Unboxed as UV
    import Data.Bits
    
    foo :: Int
    foo = UV.foldl (\s i -> if i .&. (shift 1 (i `rem` 4)) /= 0 then s+1 else s) 0 (UV.enumFromN 0 123456789)
    
    main = print foo
    

    原始代码(尽管有两个更改:rem 而不是 cmets 中建议的 mod,并在签名中添加 Int 以避免 Integer)给出:

    $ time ./orig 
    30864196
    
    real    0m2.159s
    user    0m2.144s
    sys     0m0.008s
    

    上面给出的修改代码:

    $ time ./new 
    30864196
    
    real    0m1.450s
    user    0m1.440s
    sys     0m0.004s
    

    LLVM

    虽然 LLVM 对原始代码没有太大影响,但对修改后的代码确实有影响(我很想知道为什么...)。

    原始(LLVM):

    $ time ./orig-llvm 
    30864196
    
    real    0m2.047s
    user    0m2.036s
    sys     0m0.008s
    

    修改后的(LLVM):

    $ time ./new-llvm 
    30864196
    
    real    0m0.233s
    user    0m0.228s
    sys     0m0.004s
    

    作为比较,OP 的原始 C 代码在我的系统上以 0m0.152s 用户出现。

    这都是 GHC 7.4.1、GCC 4.6.3 和向量 0.9.1。 LLVM 是 2.9 或 3.0;我两者都有,但似乎无法弄清楚实际使用的是哪一种 GHC。

    【讨论】:

    • 我将补充一点,only 用折叠替换过滤器和长度(即保留列表而不使用向量)实际上给了我一点 使用 LLVM 时,性能比原始代码差:ca。 2.6 秒。使用 LLVM,我得到了 ca。 1.4s,这与在不使用 LLVM 的情况下使用向量的改进大致相同。
    【解决方案4】:

    试试这个:

    import Data.Bits
    main = do
        print $ length $ filter (\i -> i .&. (shift 1 (i `rem` 4)) /= 0) [0..123456789::Int]
    

    没有::Int,类型默认为::Integerrem 在正值上与 mod 相同,与 C 中的 % 相同。另一方面,mod 在负值上在数学上是正确的,但速度较慢。

    • int 在 C 中是 32 位的
    • Haskell 中的 Int 是 32 位或 64 位宽,就像 C 中的 long 一样
    • Integer 是一个任意位整数,它没有最小值/最大值,它的内存大小取决于它的值(类似于字符串)。

    【讨论】:

      猜你喜欢
      • 2011-12-03
      • 2020-07-01
      • 1970-01-01
      • 2013-09-14
      • 2014-02-18
      • 1970-01-01
      • 1970-01-01
      • 2018-08-13
      • 2021-04-23
      相关资源
      最近更新 更多