什么是空间和尾部的空间复杂性?

时间:2015-04-01 14:25:58

标签: haskell profiling singly-linked-list space-complexity

TL; DR

在阅读了关于 persistence 的文章后,在Okasaki的纯功能数据结构中阅读了关于单链表的说明性例子(这就是Haskell&#39 ; s列表已实施),我想知道Data.List inits tailstails ...

的空间复杂性

在我看来

  • inits的空间复杂度在其参数的长度上线性,并且
  • tails的空间复杂度在其参数长度中二次

但是一个简单的基准表示不然。

原理

使用tails xs,可以共享原始列表。计算xs只需沿着列表xs行走,并创建一个指向该列表中每个元素的新指针;无需在内存中重新创建inits xs的一部分。

相反,因为xs"的每个元素以不同的方式结束",所以不能有这样的共享,并且必须从-- Main.hs import Data.List (inits, tails) main = do let intRange = [1 .. 10 ^ 4] :: [Int] print $ sum intRange print $ fInits intRange print $ fTails intRange fInits :: [Int] -> Int fInits = sum . map sum . inits fTails :: [Int] -> Int fTails = sum . map sum . tails 重新创建所有可能的前缀在记忆中划伤。

基准

下面的简单基准测试显示,两个功能之间的内存分配没有太大区别:

Main.hs

编译我的ghc -prof -fprof-auto -O2 -rtsopts Main.hs 文件后
./Main +RTS -p

并正在运行

Main.prof

COST CENTRE MODULE %time %alloc fInits Main 60.1 64.9 fTails Main 39.9 35.0 文件报告以下内容:

fInits

fTails分配的内存和为tails分配的内存具有相同的数量级......哼......

发生了什么事?

  • 我对inits(线性)和fInits(二次)空间复杂度的结论是否正确?
  • 如果是这样,为什么GHC会为fTails和{{1}}分配大致相同的内存? list fusion 与此有关吗?
  • 或者我的基准有缺陷?

1 个答案:

答案 0 :(得分:2)

Haskell报告中inits的实现与基本4.7.0.1(GHC 7.8.3)的实现相同或几乎完全相同,速度非常慢。特别是,fmap应用程序递归堆叠,因此强制结果的连续元素变得越来越慢。

inits [1,2,3,4] = [] : fmap (1:) (inits [2,3,4])
 = [] : fmap (1:) ([] : fmap (2:) (inits [3,4]))
 = [] : [1] : fmap (1:) (fmap (2:) ([] : fmap (3:) (inits [4])))
....

Bertram Felgenhauer探索的最简单的渐近最优实现是基于将take应用于相继更大的参数:

inits xs = [] : go (1 :: Int) xs where
  go !l (_:ls) = take l xs : go (l+1) ls
  go _  []     = []

Felgenhauer使用take的私有非融合版本能够获得额外的性能,但它仍然没有尽可能快。

以下非常简单的实现在大多数情况下要快得多:

inits = map reverse . scanl (flip (:)) []

在一些奇怪的极端情况下(如map head . inits),这个简单的实现是渐近非最优的。因此,我使用相同的技术编写了一个版本,但基于Chris Okasaki的Banker队列,这是渐近最优和几乎同样快的。 Joachim Breitner进一步优化了它,主要是使用严格的scanl'而不是通常的scanl,这个实现进入了GHC 7.8.4。 inits现在可以在O(n)时间内产生结果的脊柱;强制整个结果需要O(n ^ 2)时间,因为在不同的初始段之间不能共享任何一个。如果你想要真正荒谬的initstails,最好的办法是使用Data.Sequence; Louis Wasserman的实施is magical。另一种可能性是使用Data.Vector - 它可能会使用切片来处理这类事情。