Warning: file_get_contents(/data/phpspider/zhask/data//catemap/1/cassandra/3.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Haskell 对一大堆数字求和太慢了_Haskell_Ghc - Fatal编程技术网

Haskell 对一大堆数字求和太慢了

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

任务:“将前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-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