【问题标题】:Problems with enforcing strictness in haskell在haskell中执行严格的问题
【发布时间】:2015-08-06 05:38:27
【问题描述】:

如果想假装 Haskell 是严格的并且我有一个算法利用惰性(例如它不使用无限列表),如果我只使用会出现什么问题严格的数据类型并注释我使用的任何函数,使其参数严格?是否会有性能损失,如果有,有多糟糕;会出现更严重的问题吗?我知道盲目地让每个函数和数据类型变得严格是肮脏、毫无意义和丑陋的,我不打算在实践中这样做,但我只想了解如果这样做,Haskell 默认会变得严格吗?

其次,如果我淡化这种偏执,只对数据结构进行严格的处理:只有在我使用某种形式的累加时,我才需要担心由惰性实现引起的空间泄漏 ?换句话说,假设该算法在严格的语言中不会出现空间泄漏。还假设我在 Haskell 中使用 only 严格的数据结构实现了它,但是小心地使用 seq 来评估在递归中传递的任何变量,或者使用内部小心执行该操作的函数(如折叠'),我会避免任何空间泄漏吗?请记住,我假设在严格的语言中,相同的算法不会导致空间泄漏。所以这是一个关于lazy和strict的实现区别的问题。

我问第二个问题的原因是因为除了试图通过使用惰性数据结构或脊椎严格的数据结构来利用惰性的情况之外,我到目前为止看到的所有空间泄漏示例,只涉及在累加器中开发的 thunk,因为它不是递归调用的函数,在将自身应用于累加器之前没有评估累加器。我知道,如果一个人想利用懒惰,那么必须格外小心,但在默认情况下严格的语言中也需要这种谨慎。

谢谢。

【问题讨论】:

  • 所有这些问题都很好:这取决于 - 是的,我知道这很蹩脚 - 但你真的应该更具体一点 - 让我们看看你对 `seq 的评论' 一会儿:这只会评估为 WHNF 所以也许你不会获得太多,...... - 对不起,但这个问题只是为了给出一个很好的答案 - 至少在我看来:( - 这就是为什么我会忽略这些东西,直到你遇到麻烦 - 然后看看是什么导致它并采取相应的行动 - btw:这就是为什么你不像在严格的语言中那样使用累加器;)
  • 我知道它会评估为 WHNF,但我只使用严格的数据结构。我知道过早优化不好,但我想从理论的角度知道,我是否会面临任何问题。
  • 您最终可能会使用更少的内存(例如,避免 thunk 积累,如您的示例所示)或更多的内存,例如foldl' (+) 0 $ map (+1) [1..1000] 在严格的实现中分配 2000 个列表单元(当然,除非进行了一些优化)。在未优化的惰性实现中,我们仍然有 2000 个单元,但它们可以更早地被垃圾收集,因此在任何时候只有固定时间在内存中。
  • @chi,是的,根据情况,懒惰确实会带来优势。但是,假设我的算法不依赖于惰性,那么对我的所有函数和数据类型添加严格性并没有什么坏处?如果 Haskell 默认是严格的,我的情况并不比我差?
  • 如果一切都很严格,我相信您应该获得与 F# 或 Ocaml 等严格语言相当的性能。我猜它会比这些慢一些,因为运行时没有针对这种情况进行优化。但是运行时完全不同,而且相当复杂——我不深入了解它,所以我无法确定。

标签: haskell lazy-evaluation strict


【解决方案1】:

懒惰加快了速度

可能会更糟。 ++ 的朴素定义是:

xs ++ ys = case xs of (x:xs) -> x : (xs ++ ys)
                      []     -> ys

懒惰使这个 O(1),尽管它也可能添加 O(1) 处理来提取缺点。如果没有惰性,++ 需要立即进行评估,从而导致 O(n) 操作。 (如果你从未见过 O(.) 表示法,那是计算机科学从工程师那里偷来的东西:给定一个函数 f 集合 O( f(n) ) 是所有算法的集合,最终是最差的-与f(n)成比例,其中n是输入函数的位数。[形式上,存在kN,因此对于所有n > N,算法采用时间小于k * f(n)。] 所以我说懒惰使上述操作O(1)最终恒定时间,但为每次提取增加了恒定开销,而严格性使操作@ 987654333@ 或最终与列表元素的数量呈线性关系,假设这些元素具有固定大小。

这里有一些实际示例,但是 O(1) 增加的处理时间也可能“叠加”成 O(n) 依赖项,因此最明显的示例是 O(n2 ) 双向。这些示例中仍然存在差异。例如,一种效果不佳的情况是使用堆栈(后进先出,这是 Haskell 列表的样式)作为队列(先进先出)。

所以这是一个包含严格左折叠的快速库;我使用了 case 语句,以便每一行都可以粘贴到 GHCi 中(使用 let):

data SL a = Nil | Cons a !(SL a) deriving (Ord, Eq, Show)
slfoldl' f acc xs = case xs of Nil -> acc; Cons x xs' -> let acc' = f acc x in acc' `seq` slfoldl' f acc' xs'
foldl' f acc xs = case xs of [] -> acc; x : xs' -> let acc' = f acc x in acc' `seq` foldl' f acc' xs'
slappend xs ys = case xs of Nil -> ys; Cons x xs' -> Cons x (slappend xs' ys)
sl_test n = foldr Cons Nil [1..n]
test n = [1..n]
sl_enqueue xs x = slappend xs (Cons x Nil)
sl_queue = slfoldl' sl_enqueue Nil
enqueue xs x = xs ++ [x]
queue = foldl' enqueue []

这里的技巧是queuesl_queue 都遵循xs ++ [x] 模式将一个元素附加到列表的末尾,这需要一个列表并构建该列表的精确副本。然后 GHCi 可以运行一些简单的测试。首先,我们制作两个项目并强制他们的 thunk 来证明这个操作本身非常快并且在内存中过于昂贵:

*Main> :set +s
*Main> let vec = test 10000; slvec = sl_test 10000
(0.02 secs, 0 bytes)
*Main> [foldl' (+) 0 vec, slfoldl' (+) 0 slvec]
[50005000,50005000]
(0.02 secs, 8604632 bytes)

现在我们进行实际测试:对队列版本求和:

*Main> slfoldl' (+) 0 $ sl_queue slvec
50005000
(22.67 secs, 13427484144 bytes)
*Main> foldl' (+) 0 $ queue vec
50005000
(1.90 secs, 4442813784 bytes)

请注意,这两个在内存性能方面都很糟糕(列表附加的东西仍然秘密地是 O(n2)),它们最终占用了千兆字节空间,但严格的版本却占用了三倍的空间和十倍的时间。

有时应该更改数据结构

如果您真的想要一个严格的队列,有几个选择。一个是手指树,就像Data.Sequence 一样——viewr 他们做事的方式有点复杂,但可以获取最右边的元素。然而,这有点繁重,一个常见的解决方案是 O(1) 摊销:定义结构

data Queue x = Queue !(SL x) !(SL x)

SL 术语是上面的严格堆栈。定义一个严格的reverse,我们称之为slreverse,显而易见的方式,然后考虑:

enqueue :: Queue x -> x -> Queue x
enqueue (Queue xs ys) el = Queue xs (Cons el ys)

dequeue :: Queue x -> Maybe (x, Queue x)
dequeue (Queue Nil Nil) = Nothing
dequeue (Queue Nil (Cons x xs)) = Just (x, Queue (slreverse xs) Nil)
dequeue (Queue (Cons x xs ys)) = Just (x, Queue xs ys)

这是“摊销 O(1)”:每次dequeue 反转列表,花费一些 k 的 O(k) 步,我们确保我们正在创建一个不需要的结构为k 更多步骤支付这些费用。

懒惰隐藏错误

另一个有趣的点来自数据/余数据的区别,其中数据是通过子单元递归遍历的有限结构(即每个数据表达式都停止),而余数据是其余结构——严格列表与流。事实证明,当你正确地做出这种区分时,严格数据和惰性数据之间没有形式上的区别——严格和惰性数据之间唯一形式上的区别是它们如何处理自身内部无限循环的术语: strict 会探索循环,因此也会无限循环,而lazy 会简单地继续执行无限循环,而不深入其中。

因此,您会发现 x = slhead (Cons x undefined) 将在 head (x : undefined) 成功的地方失败。因此,当您执行此操作时,您可能会“发现”隐藏的无限循环或错误。

“一切都严格”时要小心

当您在您的语言中使用严格的数据结构时,并非所有内容都必须变得严格:请注意,我在上面指出了为列表和严格列表定义严格的foldl,而不是foldl。 Haskell 中的常见数据结构将是惰性的——列表、元组、流行库中的东西——以及对 seq 的显式调用仍然有助于构建复杂的表达式。

【讨论】:

  • “当你在你的语言中使用严格的数据结构时,并不是所有的东西都会变得懒惰:”你的意思是不是所有的东西都一定会变得 strict
  • @Reg:确实,这是错误的,我已经纠正了。
  • 懒惰确实很有用,我个人喜欢 Haskell 默认是懒惰的,因为关闭懒惰似乎在最坏的情况下只是一个小不便。似乎许多对 Haskell 默认选择惰性的批评实际上是将惰性与严格性混合在一起,即使在默认情况下打开惰性的严格语言中,这也是一个问题,就像使用脊柱严格列表一样。否则,可以选择不同级别的空间泄漏防护措施,例如严格所有内容、仅数据结构、仅需要的内容。
猜你喜欢
  • 2014-12-14
  • 2014-10-06
  • 1970-01-01
  • 1970-01-01
  • 2012-11-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-03-29
相关资源
最近更新 更多