为什么延迟模式匹配splitAt函数的版本更快?

时间:2017-02-10 02:42:43

标签: haskell recursion pattern-matching

splitAt函数可以实现如下(https://wiki.haskell.org/Lazy_pattern_match):

import Prelude hiding (splitAt)
splitAt :: Int -> [a] -> ([a], [a])
splitAt n xs =
  if n<=0
     then ([], xs)
     else
        case xs of
          [] -> ([], [])
          y:ys ->
            case splitAt (n-1) ys of
              ~(prefix, suffix) -> (y : prefix, suffix) -- Here using lazy pattern match

main :: IO ()
main = do
  let r = splitAt 1000000 $ repeat 'a'
  print $ length $ fst r

使用严格模式匹配可以大大降低速度。

time ./lazy     -- 0.04s user 0.00s system 90% cpu 0.047 total
time ./strict   -- 0.12s user 0.02s system 96% cpu 0.147 total

我无法理解。根据文章,严格版本可能需要更多内存和所有递归调用来检查模式是否适合。但我认为懒惰版本还需要所有递归调用,并且需要内存来包含递归函数调用的结果。是什么造成了这些差异?

1 个答案:

答案 0 :(得分:10)

存在许多差异。

让我们看一下第11行有和没有~的变体之间的操作差异。

GHC中的评估Haskell由模式匹配驱动。当模式在case表达式或函数定义的LHS中匹配时,它需要评估模式中的构造函数。 (let和where中的模式被视为惰性模式匹配。)这意味着评估splitAt 1000000 (repeat 'a')取决于匹配(,)的递归调用产生的splitAt 999999 ...构造函数,依此类推,一直到splitAt 0 ...的最后一次调用。这需要堆栈空间来评估。事实上,相当多。堆栈必须多次生长才能避免崩溃。

此外,在"aaaaa..."开始处理它之前,整个结果字符串length都是在堆上构建的。 (由于repeat中的优化,结果的第二个元素实际上是一个循环链表,它从不在整个递归计算中分配任何新的东西。)

当模式匹配变得懒惰时,事情会发生变化。 splitAt 1000000 (repeat 'a')的返回值被评估为('a':_thunk1, _thunk2),而不会对splitAt进行递归调用。这是一种被称为保护的核心运动的模式。进一步的评估隐藏在(,)(:)之类的数据构造函数之后,因此只有在另一个案例表达式要求时才会执行。

fst的调用会抛弃_thunk2,因此不会对其进行评估。对length的调用首先使用第一个(:)构造函数,抛出'a'值,然后在_thunk1上进行递归调用。此时,内存中没有任何内容仍然指向(:)构造函数,因此垃圾收集器可以在下次运行时自由回收它。 ('a'值是共享的,因此仍有指针,因此在此过程中既不会收集也不会分配。)

评估_thunk1时会发生什么有点微妙。它对splitAt 999999 ...进行递归调用。它导致('a':_thunk3, _thunk4)。没有任何内容可以保留_thunk4,因此无论何时都可以随意收集垃圾。 length的评估如上所述。 (:)构造函数不再保留在内存中,可以自由收集。

评估以这种方式进行,一次只能保持堆上的单个(:)构造函数,并且根本不会烧掉任何堆栈空间。 GHC的垃圾收集器的运行时间取决于驻留集的大小。由于最多只有一个(:)构造函数驻留,因此在这种情况下它真正

我怀疑在这种情况下,这就是你所看到的速度差异。您应该尝试使用参数+RTS -s运行这两个程序,并查看有关最大驻留大小和垃圾收集器运行时间的统计信息。

尽管如此,GHC可能真的聪明。我没有检查过,但我知道在某些情况下它可以根据build函数在显式(:)应用程序方面重写算法。如果要这样做,它将允许foldr / build fusion完全删除(:)构造函数的分配。 (是的,length是根据foldr通过一些非常酷的效率技巧来定义的,主要是为了让foldr / build fusion能够工作。)

如果是这种情况,你会发现在懒惰情况下发生的分配更少 - 但严格的情况也同样糟糕。我不认为这可能发生在这里,但我不确定,因为我没有测试过。