Haskell 对一大堆数字求和太慢了
任务:“将前15000000个偶数相加。” 哈斯克尔:Haskell 对一大堆数字求和太慢了,haskell,ghc,Haskell,Ghc,任务:“将前15000000个偶数相加。” 哈斯克尔: nats = [1..] :: [Int] evens = filter even nats :: [Int] MySum:: Int MySum= sum $ take 15000000 evens …但是MySum需要很长时间。更准确地说,大约比C/C++慢10-20倍 很多时候我发现,自然编码的Haskell解决方案比C慢10倍左右。我认为GHC是一个非常整洁的优化编译器,这样的任务看起来并不那么困难 因此,人们会期望比C慢1.5
nats = [1..] :: [Int]
evens = filter even nats :: [Int]
MySum:: Int
MySum= sum $ take 15000000 evens
…但是MySum需要很长时间。更准确地说,大约比C/C++慢10-20倍
很多时候我发现,自然编码的Haskell解决方案比C慢10倍左右。我认为GHC是一个非常整洁的优化编译器,这样的任务看起来并不那么困难
因此,人们会期望比C慢1.5-2倍。问题在哪里
这个问题能解决得更好吗?
这是我比较的C代码:
long long sum = 0;
int n = 0, i = 1;
for (;;) {
if (i % 2 == 0) {
sum += i;
n++;
}
if (n == 15000000)
break;
i++;
}
编辑1:我真的知道,它可以用O(1)来计算。请抵制
编辑2:我真的知道,even是
[2,4..]
,但是函数甚至可能是其他的O(1)
,需要作为一个函数来实现。严格版本工作得更快:
foldl' (+) 0 $ take 15000000 [2, 4..]
前15000000个偶数之和:
{-# LANGUAGE BangPatterns #-}
g :: Integer -- 15000000*15000001 = 225000015000000
g = go 1 0 0
where
go i !a c | c == 15000000 = a
go i !a c | even i = go (i+1) (a+i) (c+1)
go i !a c = go (i+1) a c
应该是最快的。如果要确保只遍历列表一次,可以显式编写遍历:
nats = [1..] :: [Int]
requiredOfX :: Int -> Bool -- this way you can write a different requirement
requiredOfX x = even x
dumbSum :: Int
dumbSum = dumbSum' 0 0 nats
where dumbSum' acc 15000000 _ = acc
dumbSum' acc count (x:xs)
| requiredOfX x = dumbSum' (acc + x) (count + 1) xs
| otherwise = dumbSum' acc (count + 1) xs
首先,你可以很聪明地计算O(1)中的和
除了好玩的东西,Haskell解决方案使用列表。我很确定你的C/C++解决方案不会。(Haskell列表非常容易使用,因此即使在可能不合适的情况下也会尝试使用它们。)尝试进行基准测试:
sumBy2 :: Integer -> Integer
sumBy2 = f 0
where
f result n | n <= 1 = result
| otherwise = f (n + result) (n - 2)
您还可以轻松地将过滤函数设置为参数:
sumFilter :: (Integral a) => (a -> Bool) -> a -> a
sumFilter filtfn = f 0
where
f result n | n <= 0 = result
| filtfn n = f (n + result) (n - 1)
| otherwise = f result (n - 1)
sumFilter::(积分a)=>(a->Bool)->a->a
sumFilter filtfn=f 0
哪里
f结果n | n列表不是循环
因此,如果使用列表作为循环替换,那么不要感到惊讶,如果循环体很小,那么代码会变慢
nats = [1..] :: [Int]
evens = filter even nats :: [Int]
dumbSum :: Int
dumbSum = sum $ take 15000000 evens
sum
不是一个“好消费者”,因此GHC(尚未)能够完全消除中间列表
如果使用优化编译(并且不导出nat
),GHC足够聪明,可以将过滤器
与枚举相融合
Rec {
Main.main_go [Occ=LoopBreaker]
:: GHC.Prim.Int# -> GHC.Prim.Int# -> [GHC.Types.Int]
[GblId, Arity=1, Caf=NoCafRefs, Str=DmdType L]
Main.main_go =
\ (x_aV2 :: GHC.Prim.Int#) ->
let {
r_au7 :: GHC.Prim.Int# -> [GHC.Types.Int]
[LclId, Str=DmdType]
r_au7 =
case x_aV2 of wild_Xl {
__DEFAULT -> Main.main_go (GHC.Prim.+# wild_Xl 1);
9223372036854775807 -> n_r1RR
} } in
case GHC.Prim.remInt# x_aV2 2 of _ {
__DEFAULT -> r_au7;
0 ->
let {
wild_atm :: GHC.Types.Int
[LclId, Str=DmdType m]
wild_atm = GHC.Types.I# x_aV2 } in
let {
lvl_s1Rp :: [GHC.Types.Int]
[LclId]
lvl_s1Rp =
GHC.Types.:
@ GHC.Types.Int wild_atm (GHC.Types.[] @ GHC.Types.Int) } in
\ (m_aUL :: GHC.Prim.Int#) ->
case GHC.Prim.<=# m_aUL 1 of _ {
GHC.Types.False ->
GHC.Types.: @ GHC.Types.Int wild_atm (r_au7 (GHC.Prim.-# m_aUL 1));
GHC.Types.True -> lvl_s1Rp
}
}
end Rec }
您可以得到C和您期望的Haskell版本之间运行时间的近似关系
这种算法并不是GHC教给优化的,在有限的人力投入到这些优化中之前,其他地方还有更重要的事情要做。列表融合在这里无法工作的问题实际上相当微妙。假设我们定义了正确的规则
,将列表融合在一起:
import GHC.Base
sum2 :: Num a => [a] -> a
sum2 = sum
{-# NOINLINE [1] sum2 #-}
{-# RULES "sum" forall (f :: forall b. (a->b->b)->b->b).
sum2 (build f) = f (+) 0 #-}
(简短的解释是,我们将sum2
定义为sum
的别名,我们禁止GHC提前内联,因此规则在sum2
被消除之前有机会触发。然后我们直接在列表生成器构建旁边查找sum2
(请参阅)并用直接算法代替。)
这取得了喜忧参半的成功,因为它产生了以下核心:
Main.$wgo =
\ (w_s1T4 :: GHC.Prim.Int#) ->
case GHC.Prim.remInt# w_s1T4 2 of _ {
__DEFAULT ->
case w_s1T4 of wild_Xg {
__DEFAULT -> Main.$wgo (GHC.Prim.+# wild_Xg 1);
15000000 -> 0
};
0 ->
case w_s1T4 of wild_Xg {
__DEFAULT ->
case Main.$wgo (GHC.Prim.+# wild_Xg 1) of ww_s1T7 { __DEFAULT ->
GHC.Prim.+# wild_Xg ww_s1T7
};
15000000 -> 15000000
}
}
这是很好的,完全融合的代码-唯一的问题是我们在非尾部调用位置调用$wgo
。这意味着我们不是在看循环,而是在看一个具有可预测程序结果的深度递归函数:
Stack space overflow: current size 8388608 bytes.
这里的根本问题是Prelude的列表融合只能融合右折叠,而将总和计算为右折叠直接导致堆栈消耗过多。
显而易见的解决办法是使用一个融合框架,该框架实际上可以处理左折叠,例如Duncan的,它实际上实现了sum
融合
另一个解决方案是绕过它——并使用右折叠实现左折叠:
main = print $ foldr (\x c -> c . (+x)) id [2,4..15000000] 0
这实际上为当前版本的GHC生成了近乎完美的代码。另一方面,这通常不是一个好主意,因为它依赖于GHC足够聪明来消除部分应用的功能。在链中添加一个过滤器
将打破这种特定的优化。另一件需要注意的事情是nats
和evens
是所谓的常量应用形式,简称CAF。基本上,它们对应于没有任何参数的顶级定义。咖啡馆是一个有点奇怪的鸭子,例如,是可怕的单态限制的原因;我不确定语言定义是否允许将CAF内联
在我关于Haskell如何执行的心智模型中,当dumbSum
返回一个值时,evens
将被评估为类似2:4:…:30000000:
和nats
到1:2:…:30000000:
,其中
表示尚未查看的内容。如果我的理解是正确的,这些:
的分配是必须发生的,并且不能被优化掉
因此,在不过度修改代码的情况下加快速度的一种方法是简单地编写:
dumbSum :: Int
dumbSum = sum . take 15000000 . filter even $ [1..]
或
在我用-O2
编译的机器上,仅此一项似乎就可以带来大约30%的加速
我不是GHC鉴赏家(我甚至从来没有分析过Haskell项目!),所以我可能会大错特错。不,不幸的是,它没有。至少在我的机器上。[2,4…],不,甚至
功能也是必不可少的。@Martin:当然应该。你是如何编译你的程序的?试试看<代码>ghc-fllvm-optc-O2-O2 foo.hs-fforce recomp
@Sarah你低估了ghc。由于该类型被指定为Int
,它使另一个版本本身变得严格,因此与foldl'(+)0
没有区别。但是,如果这是正确的解决方案,我们可以将Haskell扔出窗口,继续使用C
。GHC很可能以这种方式优化代码,但通过这种方式,您可以对实现进行客观比较。此外,我认为,即使我们需要编写这样的函数,使用Haskell也有很多令人信服的理由。@Martin当然-如果您的程序只需要将偶数相加,那么您用什么语言编写它几乎没有什么区别。@Martin等等。。你不能
main = print $ foldr (\x c -> c . (+x)) id [2,4..15000000] 0
dumbSum :: Int
dumbSum = sum . take 15000000 . filter even $ [1..]
dumbSum = sum $ take 15000000 evens where
nats = [1..]
evens = filter even nats