我有这个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);
}
答案 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对原始代码没有产生巨大影响,但它确实对修改过了(我很想知道为什么......)。
好吧,原始代码使用 列表,llvm不太了解如何处理这些,它无法将该代码转换为循环。修改后的代码使用Int
和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
不会检查其参数。
使用-fllvm
和i
进行编译,这比我机器上的原始版本快13倍。
注意:测试x
的{{1}}位是否设置可以更清楚地写为x `testBit` i
。这产生与上面相同的组件。
答案 2 :(得分:12)
将列表替换为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):
$ 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
,则类型默认为::Integer
。
rem
与正值上的mod
相同,并且与C中的%
相同。另一方面,mod
在负值上在数学上是正确的,但是慢。
int
是32位Int
为32或64位宽,如C中的long
Integer
是一个任意位整数,它没有最小值/最大值,其内存大小取决于它的值(类似于字符串)。