任务:“总计前15,000,000个偶数。”
Haskell中:
nats = [1..] :: [Int]
evens = filter even nats :: [Int]
MySum:: Int
MySum= sum $ take 15000000 evens
...但MySum
需要很长时间。更准确地说,比C / C ++慢大约10-20倍。
所以,人们会期望比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 :我真的知道,evens是[2,4..]
,但函数even
可能是其他O(1)
,需要作为函数实现
答案 0 :(得分:24)
因此,如果使用列表作为循环替换,请不要感到惊讶,如果循环体很小,则会得到较慢的代码。
nats = [1..] :: [Int]
evens = filter even nats :: [Int]
dumbSum :: Int
dumbSum = sum $ take 15000000 evens
sum
不是“好消费者”,因此GHC(还)无法完全消除中间列表。
如果使用optimisations进行编译(并且不导出nat
),GHC足够聪明地将filter
与枚举融合,
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 }
但就GHC的融合而言,这就是它。您将获得装箱Int
并构建列表单元格。如果你给它一个循环,就像你把它交给C编译器一样,
module Main where
import Data.Bits
main :: IO ()
main = print dumbSum
dumbSum :: Int
dumbSum = go 0 0 1
where
go :: Int -> Int -> Int -> Int
go sm ct n
| ct >= 15000000 = sm
| n .&. 1 == 0 = go (sm + n) (ct+1) (n+1)
| otherwise = go sm ct (n+1)
你得到了你期望的C和Haskell版本之间的运行时间的近似关系。
这种算法并不是GHC所教导的优化效果,在将有限的人力投入到这些优化之前,还有更大的鱼可以在别处煎炸。
答案 1 :(得分:11)
列表融合在这里无法解决的问题实际上相当微妙。假设我们定义了正确的RULE
来融合列表:
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尽早内联,因此RULE
有机会在{{1}之前触发然后我们直接在列表构建器sum2
旁边查找sum2
(请参阅definition)并通过直接算术替换它。)
这取得了成功,因为它产生了以下核心:
build
哪个好,完全融合的代码 - 唯一的问题是我们在非尾部调用位置调用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
这里的根本问题是Prelude的列表融合只能融合正确的折叠,并且将总和计算为右折叠直接导致过多的堆栈消耗。
显而易见的解决方法是使用可以实际处理左侧折叠的融合框架,例如Duncan的stream-fusion package,它实际上实现了Stack space overflow: current size 8388608 bytes.
融合。
另一种解决方案是破解它 - 并使用右侧折叠实现左侧折叠:
sum
这实际上产生了与当前版本的GHC接近完美的代码。另一方面,这通常不是一个好主意,因为它依赖于GHC足够聪明以消除部分应用的功能。已经在链中添加main = print $ foldr (\x c -> c . (+x)) id [2,4..15000000] 0
将破坏该特定优化。
答案 2 :(得分:5)
首先汇总15,000,000个偶数:
{-# 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
应该是最快的。
答案 3 :(得分:4)
如果你想确保只遍历一次列表,你可以明确地编写遍历:
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
答案 4 :(得分:3)
首先,你可以聪明地young Gauss was并计算 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)
使用GHC与-O2
argument进行编译。这个函数是tail-recursive,因此编译器可以非常有效地实现它。
更新:如果您希望使用even
功能,则可能:
sumBy2 :: Integer -> Integer
sumBy2 = f 0
where
f result n | n <= 0 = result
| even n = f (n + result) (n - 1)
| otherwise = f result (n - 1)
您还可以轻松地将过滤功能设为参数:
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)
答案 5 :(得分:2)
严格的版本运行得更快:
foldl' (+) 0 $ take 15000000 [2, 4..]
答案 6 :(得分:1)
需要注意的另一点是,nats
和evens
是所谓的常规适用表格,简称CAF。基本上,那些对应于没有任何参数的顶级定义。 CAF有点奇怪,例如是可怕的单态限制的原因;我不确定语言定义是否允许 CAF内联。
在我的Haskell如何执行的心智模型中,当dumbSum
返回一个值时,evens
将被评估为2:4: ... : 30000000 : <thunk>
和nats
到{{ 1}},其中1:2: ... : 30000000 : <thunk>
代表尚未查看的内容。如果我的理解是正确的,那么<thunk>
的这些分配必须发生,并且无法进行优化。
因此,在不改变代码的情况下加快速度的一种方法就是简单地写:
:
或
dumbSum :: Int
dumbSum = sum . take 15000000 . filter even $ [1..]
在我的机器上,使用dumbSum = sum $ take 15000000 evens where
nats = [1..]
evens = filter even nats
进行编译,仅此一项似乎可以带来大约30%的加速。
我不是GHC的骗子(我从来没有描述过一个Haskell程序!),所以我可能会大打折扣。