繁体   English   中英

Haskell:地图长度。 group 比显式递归慢吗?

[英]Haskell: map length . group is way slower than explicit recursion?

考虑这个整数n素数分解的简单算法:让d'是最后找到的n的除数。 最初,设置d'=1 寻找最小除数d>d'n ,并找到最大值e使得d e分歧n d e附加到答案并在n/d e上重复该过程。 最后,当n变为 1 时停止。为了简单起见,让我们忽略数学优化,例如在sqrt n处停止等。

我已经通过两种方式实现了它。 第一个生成除法“尝试”列表,然后按除数对成功的进行分组。 例如,对于n=20 ,我们首先生成[(2,20),(2,10),(2,5),(3,5),(4,5),(5,5),(5,1)] ,然后我们使用group和其他库函数将其转换为所需的[(2,2),(5,1)]

第二种实现是显式递归,它一路跟踪指数e ,一旦达到最大值e就将d e附加到答案,继续寻找“下一个” d ,依此类推。

问题 1:尽管有以下情况,为什么第一个实现的运行速度比第二个慢:

  • 两种实现都执行算法的核心步骤div的次数大致相同。
  • 懒惰评估(和融合?)的效果是上面说明的长列表永远不必首先实现。 正如您在下面的代码中看到的那样,我正在谈论的列表divTrials n由一系列高阶函数转换。 在那,我认为部分map (\\xs-> (head xs,length xs)) ... group应该告诉编译器列表只是中间的:
{-# OPTIONS_GHC -O2 #-}
module GroupCheck where
import Data.List
import Data.Maybe

implement1 :: Integral t=> t -> [(t,Int)]                    -- IMPLEMENTATION 1
implement1  = map (\xs-> (head xs,length xs)).factorGroups where
  tryDiv (d,n)
    | n `mod` d == 0 = (d,n `div` d)
    | n == 1 = (1,1) -- hack
    | otherwise = (d+1,n)
  divTrials n = takeWhile (/=(1,1)) $ (2,n): map tryDiv (divTrials n)
  factorGroups = filter (not.null).map tail.group.map fst.divTrials

implement2 :: Show t => Integral t => t -> [(t,Int)]         -- IMPLEMENTATION 2
implement2 num = keep2 $ tail $ go (1,0,1,num) where
  range d n = [d+1..n]
  nextd d n = fromMaybe n $ find ((0==).(n`mod`)) (range d n)
  update (d,e,de,n)
    | n `mod` d == 0 = update (d,e+1,de*d,n`div`d)
    | otherwise      = (d,e,de,n)
  go (d,e,de,1) = [(d,e,de,1)]
  go (d,e,de,n) = (d,e,de,n) : go (update (nextd d n,0,1,n))
  keep2 = map (\(d,e,_,_)->(d,e))

main :: IO ()
main = do
  let n = 293872
  let ans1 = implement1 n 
  let ans2 = implement2 n
  print ans1
  print ans2

分析告诉我们tryDivdivTrials一起占用了整个执行时间的 99% 以上:

> stack ghc -- -main-is GroupCheck.main -prof -fprof-auto -rtsopts GroupCheck 
> ./GroupCheck +RTS -p >/dev/null && cat GroupCheck.prof


           GroupCheck +RTS -p -RTS

        total time  =       18.34 secs   (18338 ticks @ 1000 us, 1 processor)
        total alloc = 17,561,404,568 bytes  (excludes profiling overheads)

COST CENTRE          MODULE     SRC                          %time %alloc

implement1.divTrials GroupCheck GroupCheck.hs:12:3-69         52.6   69.2
implement1.tryDiv    GroupCheck GroupCheck.hs:(8,3)-(11,25)   47.2   30.8

问题 1.5:那么……这些函数有什么不好? 还,

问题 2:在更一般的情况下,必须从非递减序列中聚合相同元素的连续块,如果我们想要速度,我们应该采用笨重的implement2方式吗? (同样,忽略特定领域的优化。)

还是我完全错过了一些明显的东西? 谢谢!

只是为了建立一个基线,我在一个稍大的起始数字上运行你的程序(这样time就不会打印出 0.00 秒)。 我没有特别的原因选择了n = 2938722345623 这是开始调整之前的时间安排:

ans1 : 与无穷大ans1区别(我写完了整个答案,它仍在运行,总共大约 26 分钟)
ans2 :2.78s

首先要尝试的是调整这一行:

divTrials n = takeWhile (/=(1,1)) $ (2,n): map tryDiv (divTrials n)

这看起来是一个很自然的定义,但事实证明 GHC 从不记忆函数调用。 因此,如果您想创建一个根据自身递归定义的列表,则不得在递归中进行函数调用。 就是这样:

divTrials n = xs where xs = takeWhile (/=(1,1)) $ (2,n): map tryDiv xs

正是这种变化将时间缩短到 7.85 秒。 仍然降低了大约 3 倍,但要好得多。

不太明显的问题就在这里:

factorGroups = filter (not.null).map tail.group.map fst.divTrials

这么早放group会破坏融合,导致中间名单真正实现。 这意味着分配和释放大量的 cons 单元和元组。 这是一个具有相同精神的实现,但在group面前投入了更多工作:

  tryDiv d n
    | n `mod` d == 0 = d : tryDiv d (n `div` d)
    | n == 1 = []
    | otherwise = tryDiv (d+1) n
  factorGroups = group . tryDiv 2

有了这个,我们下降到 2.65 秒——比ans2略快,尽管我只对每个测试做了一个测试,所以很可能只是测量噪声。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM