32

这现在可能有点模糊,但我一直想知道这一点。据我所知!,可以确保在构造值之前评估数据构造函数的参数:

data Foo = Bar !Int !Float

我经常认为懒惰是一件好事。现在,当我浏览源代码时,我看到的严格字段比!-less 变体更频繁。

这样做有什么好处,为什么我不应该让它保持懒惰呢?

4

4 回答 4

40

除非您在 Int 和 Float 字段中存储大量计算,否则大量的琐碎计算会在 thunk 中建立起来,从而产生巨大的开销。例如,如果您反复将 1 加到数据类型中的惰性 Float 字段,它将占用越来越多的内存,直到您实际强制该字段并计算它。

通常,您希望在字段中存储昂贵的计算。但是如果你知道你不会提前做这样的事情,你可以将字段标记为严格,并且避免必须在seq任何地方手动添加以获得你想要的效率。

作为额外的奖励,当给定标志时,-funbox-strict-fieldsGHC 会将数据类型的严格字段1直接解压缩到数据类型本身中,这是可能的,因为它知道它们将始终被评估,因此不必分配 thunk;在这种情况下,Bar 值将直接在内存中的 Bar 值中包含包含 Int 和 Float 的机器字,而不是包含两个指向包含数据的 thunk 的指针。

懒惰是一件非常有用的事情,但在某些时候,它只会妨碍计算,特别是对于总是被关注(并因此被强制)的小字段,或者经常修改但从不进行非常昂贵的计算的小字段。严格的字段有助于克服这些问题,而无需修改数据类型的所有用途。

它是否比惰性字段更常见取决于您正在阅读的代码类型;例如,您不太可能看到任何功能性树结构广泛使用严格字段,因为它们从惰性中受益匪浅。

假设您有一个带有用于中缀操作的构造函数的 AST:

data Exp = Infix Op Exp Exp
         | ...

data Op = Add | Subtract | Multiply | Divide

您不想使Exp字段变得严格,因为应用这样的策略意味着每当您查看顶级节点时都会评估整个 AST,这显然不是您希望从懒惰中受益的。但是,该Op字段永远不会包含您想要推迟到以后的昂贵计算,并且如果您有真正深度嵌套的解析树,每个中缀运算符的 thunk 开销可能会变得昂贵。因此,对于中缀构造函数,您希望使Op字段严格,但让这两个Exp字段保持惰性。

1只能解包单构造函数类型。

于 2011-12-20T14:30:54.643 回答
9

除了其他答案提供的信息外,请记住:

据我所知!,可以确保在构造值之前评估数据构造函数的参数

看看参数被评估的深度是很有趣的——就像用seq$!评估到WHNF 一样

给定数据类型

data Foo = IntFoo !Int | FooFoo !Foo | BarFoo !Bar
data Bar = IntBar Int

表达方式

let x' = IntFoo $ 1 + 2 + 3
in  x'

评估为 WHNF 产生价值IntFoo 6(== 完全评估,== NF)。
另外这个表达式

let x' = FooFoo $ IntFoo $ 1 + 2 + 3
in  x'

评估为 WHNF 产生价值FooFoo (IntFoo 6)(== 完全评估,== NF)。
然而,这个表达式

let x' = BarFoo $ IntBar $ 1 + 2 + 3
in  x'

评估为 WHNF 产生值BarFoo (IntBar (1 + 2 + 3))(!= 完全评估,!= NF)。

要点:如果数据构造函数本身不包含严格参数,则参数的严格性!Bar不一定有帮助。Bar

于 2015-06-06T12:21:02.157 回答
4

有一个与惰性相关的开销——编译器必须为值创建一个 thunk 来存储计算,直到需要结果。如果您知道迟早总是需要结果,那么强制评估结果是有意义的。

于 2011-12-20T14:32:00.940 回答
4

懒惰是有代价的,否则每一种语言都会有它。

成本是2倍:

  1. 与立即执行操作相比,设置 thunk 可能需要更长的时间(即描述最终要计算时必须计算的内容)。
  2. 未评估的 thunk 作为非严格参数传递给其他 thunk 作为非严格参数传递给其他 thunk 等,将使用越来越多的内存。不幸的是,这些 tunk 也可能持有对不再可访问的内存的引用,即当只评估 thunk 时可以释放的内存,从而阻止垃圾收集器完成其工作。一个示例是应该更新树中某个值的 thunk。假设这个值保留了 100MB 的其他值。如果不再引用旧树,则只要不评估 thunk,就会浪费此内存。
于 2011-12-20T22:01:25.807 回答