微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

seq 在 Haskell 中实际上做了什么? 严格的含义

如何解决seq 在 Haskell 中实际上做了什么? 严格的含义

Real World Haskell我读到

它的操作如下:当对 seq 表达式求值时,它强制求值它的第一个参数,然后 返回它的第二个参数。它实际上对第一个参数没有任何作用:seq 仅作为一种强制计算该值的方式存在。

我强调了 then 的地方,因为对我来说它暗示了两件事发生的顺序。

Hackage我读到

如果 seq a b底部,则 a 的值为底部,否则等于 b。换句话说,它将第一个参数 a 评估为弱头范式 (WHNF)。通常引入 seq 是为了通过避免不必要的懒惰来提高性能

关于求值顺序的注意事项:表达式 seq a b 不保证 a 会在 b 之前求值。 seq 给出的唯一保证是 ab 将在 seq 返回值之前进行评估。特别是,这意味着 b 可以在 a 之前进行评估。 […]

此外,如果我从那里点击 # Source 链接页面不存在,所以我看不到 seq代码

这似乎与 this answer 下的评论一致:

[…] seq 不能在普通 Haskell 中定义

另一方面(或者说实际上),另一条评论写道:

“真实”seq 在 GHC.Prim 中定义为 seq :: a -> b -> b; seq = let x = x in x这只是一个虚拟定义。基本上 seq 是由编译器特别处理的特殊语法

有人可以对这个话题有所了解吗?尤其是在以下方面:

  • 哪个来源是正确的?
  • seq 的实现在 Haskell 中真的不可写吗?
    • 如果是这样,那意味着什么?说它是原始的?这告诉我 seq 的实际作用是什么?
  • 至少在 seq a b 使用 a 的情况下,b 保证在 b 之前进行评估,例如a

解决方法

其他答案已经讨论了 seq 的含义及其与 pseq 的关系。但是,对于 seq 的警告的含义究竟是什么,似乎存在一些混淆。

从技术上讲,a `seq` b 确实保证 a 会在 b 之前进行评估。这似乎令人不安:如果是这样,它怎么可能达到其目的?让我们考虑一下乔恩在他们的回答中给出的例子:

foldl' :: (a -> b -> a) -> a -> [b] -> a
foldl' f acc [] = acc
foldl' f acc (x : xs)
  = acc' `seq` foldl' f acc' xs
  where
    acc' = f acc x

当然,我们关心 acc' 在递归调用之前被评估。如果不是,foldl' 的全部目的就失去了!那么为什么不在这里使用 pseq 呢? seq 真的那么有用吗?

幸运的是,情况实际上并没有那么可怕。 seq 在这里确实正确的选择。 GHC 永远不会选择编译 foldl',这样它在评估 acc' 之前评估递归调用,因此我们想要的行为被保留。 seqpseq 之间的区别在于优化器在认为有特别好的理由时必须灵活地做出不同的决定。

了解seqpseq的严格性

要理解这意味着什么,我们必须学会像 GHC 优化器一样思考。实际上,seqpseq 之间唯一的具体区别在于它们如何影响严格分析器:

  1. seq两个参数都被认为是严格的。也就是说,在像

    这样的函数定义中
    f a b c = (a `seq` b) + c
    

    f 的所有三个参数都将被视为严格。

  2. pseq 就像 seq 一样,但它只在其第一个参数中被认为是严格的,而不是它的第二个参数。这意味着在像

    这样的函数定义中
    g a b c = (a `pseq` b) + c
    

    gac 中将被视为严格,但不是 b

这是什么意思?好吧,让我们首先定义一个函数“严格控制它的一个参数”意味着什么。这个想法是,如果一个函数的一个参数是严格的,那么对该函数的调用肯定会评估该参数。这有几个含义:

  • 假设我们有一个函数 foo :: Int -> Int 的参数是严格的,并且假设我们有一个对 foo 的调用,如下所示:

    foo (x + y)
    

    一个朴素的 Haskell 编译器会为表达式 x + y 构造一个 thunk,并将生成的 thunk 传递给 foo。但我们知道,评估 foo必然迫使这种情况发生,因此我们不会从这种懒惰中获得任何好处。最好立即评估 x + y,然后将结果传递给 foo 以节省不必要的 thunk 分配。

  • 由于我们知道永远没有任何理由将 thunk 传递给 foo,因此我们有机会进行额外的优化。例如,优化器可以选择在内部重写 foo 以采用未装箱的 Int# 而不是 Int,不仅避免 x + y 的 thunk 构造,而且避免装箱结果完全值。这允许 x + y 的结果直接在堆栈上传递,而不是在堆上。

如您所见,严格性分析对于构建高效的 Haskell 编译器至关重要,因为它允许编译器就如何编译函数调用等做出更明智的决定。出于这个原因,我们通常希望严格性分析能够找到尽可能多的机会来急切地评估事物,从而节省无用的堆分配。

考虑到这一点,让我们回到上面的 fg 示例。让我们考虑一下我们直觉期望这些函数具有什么样的严格性:

  1. 回忆一下 f 的主体是 (a `seq` b) + c。即使我们完全忽略 seq 的特殊属性,我们也知道它最终会计算出它的第二个参数。这意味着 f 应该至少严格,就好像它的主体只是 b + ca 完全未使用)。

    我们知道评估 b + c 必须从根本上评估 bc,所以 f 至少必须在 bc。在 a 中是否严格是更有趣的问题。如果 seq 实际上只是 flip const,则不会,因为 a 不会被使用,但当然 seq整个点是引入了人为的严格性,所以实际上 fa 中也被认为是严格的。

    令人高兴的是,我上面提到的 f 的严格性与我们对它应该具有的严格性的直觉完全一致。 f 的所有参数都是严格的,正如我们所期望的那样。

  2. 直觉上,上述 f 的所有推理也应适用于 g。唯一的区别是用 seq 替换 pseq,而且我们知道 pseq 提供了比 seq 对评估顺序更强的保证,所以我们希望 g 至少和 f 一样严格……也就是说,它的所有参数也很严格。

    然而,值得注意的是,这不是 GHC 为 g 推断的严格性。 GHC 在 ga 中认为 c 是严格的,但不是 b,尽管根据我们上面对严格的定义,g 在 {{ 中显然是严格的1}}:b 必须 评估 b 才能产生结果!正如我们将看到的,正是这种差异使 g 如此神奇,以及为什么它通常是一个坏主意。

严格的含义

我们现在已经看到 pseq 导致了我们期望的严格性,而 seq 没有,但这意味着什么并不是很明显。为了说明这一点,请考虑使用 pseq 的可能调用站点:

f

我们知道 f a (b + 1) c 的所有参数都是严格的,因此根据我们上面使用的相同推理,GHC 应该急切地评估 f 并将其结果传递给 b + 1,避免重击.

乍一看,这似乎一切都很好,但等等:如果 f 是一个重击怎么办?尽管 af 中也很严格,但它只是一个裸变量——也许它是从其他地方作为参数传入的——而且 GHC 没有理由急切地在此处强制使用 a如果 a 将强制它自己。我们强制 f 的唯一原因是避免创建 new thunk,但我们除了在调用站点强制已创建的 b + 1 之外什么都不保存。这意味着 a 实际上可能作为未评估的 thunk 传递。

这是一个问题,因为在 a 的主体中,我们写了 f,请求 a `seq` b before a .但是根据我们上面的推理,GHC 只是先行评估了 b!如果我们真的、真的需要确保在 b 之后才评估 b,则不允许这种类型的急切评估。

当然,这正是 a 在其第二个参数中被认为是懒惰的原因,尽管实际上并非如此。如果我们用 pseq 替换 f,那么 GHC 会乖乖地为 g 分配一个新的 thunk 并将它传递到堆上,确保它不会过早地被评估。这当然意味着更多的堆分配,没有拆箱,并且(最糟糕的是)没有严格信息在调用链上进一步传播,从而产生潜在的级联悲观。但是,嘿,这就是我们的要求:不惜一切代价避免过早评估 b + 1

希望这说明了为什么 b 很诱人,但最终会适得其反,除非您真的知道自己在做什么。当然,你保证你正在寻找的评估......但代价是什么?

要点

希望上面的解释已经说明了 pseqseq 的优缺点:

  • pseq 与严格性分析器配合得很好,暴露了更多潜在的优化,但这些优化可能会破坏我们预期的评估顺序。

  • seq 不惜一切代价保留所需的评估顺序,但它只是通过完全向严格分析器撒谎来做到这一点,因此它不会出现这种情况,从而大大削弱了它帮助优化器执行此操作的能力好东西。

我们如何知道选择哪些权衡?虽然我们现在可能理解了为什么 pseq 有时无法在第二个参数之前评估它的第一个参数,但我们没有更多的理由相信这是一件可以发生的事情。

为了缓解您的恐惧,让我们退后一步,想想这里到底发生了什么。请注意,GHC 从不 实际上编译 seq 表达式本身的方式是 a `seq` ba 之前无法计算。给定像 b 这样的表达式,GHC 永远不会在评估 a `seq` (b + c) 之前暗中刺伤您并评估 b + c。相反,它所做的要微妙得多:它可能间接导致 ab 在评估整个 c 表达式之前被单独评估,因为严格分析器会注意到 b + cb 中的整体表达式仍然很严格。

如何将所有这些组合在一起是难以置信棘手的,它可能会让你头晕目眩,所以也许你根本没有发现上一段是那么舒缓。但为了更具体地说明这一点,让我们回到本答案开头的 c 示例。回想一下,它包含这样的表达式:

foldl'

为了避免 thunk 爆发,我们需要在对 acc' `seq` foldl' f acc' xs 的递归调用之前评估 acc'。但鉴于上述推理,它仍然永远是! foldl' 在这里相对于 seq 的差异再次仅与严格性分析相关:它允许 GHC 推断此表达式在 pseqf 中也是严格的,而不仅仅是 xs,在这种情况下,它实际上根本没有太大变化:

  • 整个 acc' 函数在 foldl' 中仍然不被认为是严格的,因为在函数的第一种情况(fxs ),[] 未使用,因此对于某些调用模式,ffoldl' 中是惰性的。

  • f foldl' 中可以被认为是严格的,但这在这里完全无趣,因为 xs 只是 xs 的一部分{1}} 的参数,并且严格性信息根本不影响 foldl' 的严格性。

那么,如果这里实际上没有任何区别,为什么不使用 foldl'?好吧,假设 pseq 在调用点被内联了有限次数,因为它的第二个参数的形状可能是部分已知的。 foldl' 公开的严格性信息可能会在调用站点公开几个额外的优化,从而导致一系列有利的优化。如果使用了 seq,这些优化将被掩盖,并且 GHC 会产生更糟糕的代码。

因此,真正的要点是,即使 pseq 可能有时不会在第二个参数之前评估它的第一个参数,但这只是技术上正确的,它发生的方式是微妙的,并且不太可能破坏你的程序。这应该不会太令人惊讶:seq 是 GHC 的作者期望 程序员在这种情况下使用的工具,因此让他们做错事是相当粗鲁的! seq 是此作业的惯用工具,而不是 seq,因此请使用 pseq

那么您什么时候使用 seq?只有当您真的非常关心非常具体的评估顺序时,这通常仅出于以下两个原因之一才会发生:您正在使用基于 pseq 的并行性,或者您正在使用 par 并关心副作用的顺序。如果您没有做这些事情中的任何一项,那么就不要使用 unsafePerformIO如果您只关心像 pseq 这样的用例,而您只是想避免不必要的 thunk 构建,请使用 foldl'这就是它的用途。

,

seq 在两个 thunk 之间引入了人工数据依赖。通常,仅当模式匹配需要时才强制 thunk 进行评估。如果 thunk a 包含表达式 case b of { … },则强制 a 也会强制 b。因此两者之间存在依赖关系:为了确定 a 的值,我们必须评估 b

seq 指定任意两个任意 thunk 之间的这种关系。当 seq c d 被强制时,除了 c 之外,d 也被强制。请注意,我没有说 before:根据标准,实现可以自由地强制 c before d or {{1} } 在 d 之前,甚至是它们的混合。只需要如果c不停止,那么c也不会停止。如果你想保证评估顺序,你可以使用seq c d

下图说明了差异。黑色箭头 (▼) 表示真正的数据依赖性,您可以使用 pseq 表达这种依赖性;白色箭头(▽)表示人工依赖。

  • 强制 case 必须同时强制 seq a ba

    b
  • 强制 │ ┌─▼───────┐ │ seq a b │ └─┬─────┬─┘ │ │ ┌─▽─┐ ┌─▼─┐ │ a │ │ b │ └───┘ └───┘ 必须强制 pseq a b,后者必须先强制​​ b

    a

就目前而言,它必须作为内部函数实现,因为它的类型 │ ┌─▼────────┐ │ pseq a b │ └─┬────────┘ │ ┌─▼─┐ │ b │ └─┬─┘ │ ┌─▽─┐ │ a │ └───┘ 声称它适用于任何类型 forall a b. a -> b -> ba,没有任何约束。它曾经属于一个类型类,但由于类型类版本被认为具有较差的人体工程学,因此被删除并制作为原始类型:添加 b 以尝试修复深度嵌套的函数调用链中的性能问题将需要在链中的每个函数上添加一个样板 seq 约束。 (我更喜欢明确的,但现在很难改变。)

因此,Seq a 及其语法糖(例如 seq 类型中的严格字段或模式中的 data)是为了确保通过附加来评估某些内容它对其他将被评估的东西的评估。经典示例是 BangPatterns。此处,foldl' 确保在强制递归调用时,也强制累加器:

seq

要求编译器如果 foldl' :: (a -> b -> a) -> a -> [b] -> a foldl' f acc [] = acc foldl' f acc (x : xs) = acc' `seq` foldl' f acc' xs where acc' = f acc x 是严格的,例如 f 是严格的数据类型,例如 (+),那么累加器被简化为每一步都有一个 Int,而不是构建一个只在最后评估的 thunk 链。

,

Real World Haskell 是错误的,您引用的所有其他内容都是正确的。如果您非常关心评估顺序,请改用 pseq

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。