总结一大堆数字太慢了

时间:2012-11-15 14:00:43

标签: haskell ghc

任务:“总计前15,000,000个偶数。”

Haskell中:

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 :我真的知道,evens是[2,4..],但函数even可能是其他O(1),需要作为函数实现

7 个答案:

答案 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)

需要注意的另一点是,natsevens是所谓的常规适用表格,简称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程序!),所以我可能会大打折扣。