我可以加速这个Haskell算法吗?

时间:2012-10-04 15:18:47

标签: haskell

我有这个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]

使用gcc -O2(gcc 4.6.3)编译的C中的相同算法在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);
}

4 个答案:

答案 0 :(得分:24)

这两段代码做了很多不同的事情。

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中,有一个提示,所以它不是一个制动器使用mod之前的Integer),将Integer 1移动到适当的位数,这涉及转换为“大”i和调用GMP,按位和与Integer - 再次调用GMP - 并检查结果是否为0,这导致另一次调用GMP或转换为小整数,不知道GHC在这里做了什么。然后,如果结果非零,则会创建一个新的列表单元格,其中length被放入并由Integer使用。这是一项很多完成的工作,其中大部分由于将未指定的数字类型默认为#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; } 而不必要地复杂化。

C代码

main

(我冒昧地修复了int的返回类型),做得少得多。需要 int ,将其与另一个进行比较,如果较小,则按位进行比较,将第一个int与3 (1)进行比较,左边的int 1包含适当的位数,取位数和第一个int,如果非零递增另一个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 。这些都是机器操作,处理原始机器类型。

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

4`` instead of

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

Integer

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

好的,我已经完成了使用2位幂模数替换模数计算的优化,并且手动,GHC的本机代码生成器不会那么做(还),所以使用```rem { {1}}。&安培;. 3`,本机代码生成器生成的代码需要(这里)运行1.42秒,但llvm后端执行该优化,并生成与手工优化相同的代码。

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

  

虽然LLVM对原始代码没有产生巨大影响,但它确实对修改过了(我很想知道为什么......)。

好吧,原始代码使用 Int列表,llvm不太了解如何处理这些,它无法将该代码转换为循环。修改后的代码使用vector s并且div包将代码重写为循环,因此llvm 知道如何优化它,并显示。

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

答案 1 :(得分:15)

使用严格累加器的手写循环很少:

{-# 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不会检查其参数。

使用-fllvmi进行编译,这比我机器上的原始版本快13倍。

注意:测试x的{​​{1}}位是否设置可以更清楚地写为x `testBit` i。这产生与上面相同的组件。

答案 2 :(得分:12)

Vector而不是list,fold而不是filter-and-length

将列表替换为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而不是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实际上正在使用。

答案 3 :(得分:0)

试试这个:

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

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

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