【问题标题】:Why does adding a tilde in front of a pattern match slow my function down?为什么在模式匹配前添加波浪号会减慢我的功能?
【发布时间】:2013-03-03 11:02:51
【问题描述】:

我在一段 haskell 代码中定义了两个函数:

lengthwtilde [] = 0
lengthwtilde ~(_:xs) = 1 + lengthwtilde xs

lengthwotilde [] = 0
lengthwotilde (_:xs) = 1 + lengthwotilde xs

当我在 ghci 中测试它们(使用 :set +s)时,我发现 lengthwtilde(在模式匹配前面带有波浪号的那个)的执行速度明显慢于 lengthwotilde 大约三秒。

*Main> lengthwtilde [1..10000000]
10000000
(19.40 secs, 1731107132 bytes)
*Main> lengthwotilde [1..10000000]
10000000
(16.45 secs, 1531241716 bytes)

这是为什么?

【问题讨论】:

标签: haskell


【解决方案1】:

在模式匹配前添加~ 会使匹配无可辩驳。您可以将其视为为模式添加了额外的惰性,因此它永远不会匹配失败,除非该匹配是绝对需要的评估。这是一个简单的例子:

Prelude> (\ (_:_) -> "non-empty") []
"*** Exception: <interactive>:2:2-23: Non-exhaustive patterns in lambda

Prelude> (\ ~(_:_) -> "oops") []
"oops"

使用无可辩驳的模式匹配,即使模式匹配在空列表上失败,因为没有评估绑定变量,所以没有错误。本质上,无可辩驳的模式匹配将函数转换为:

\ xs -> let (_:_) = xs in "oops"

正是这种额外的懒惰包装减慢了你的长度函数。如果你对lengthwtilde 应用相同的 let-binding 转换,你会得到

lengthwtilde [] = 0
lengthwtilde xs' = let (_:xs) = xs' in 1 + lengthwtilde xs

想想这是如何评估的。在顶层,你会得到1+lengthwtilde xs。但是 xs 甚至没有被评估,因为它是一个 let-bound 变量。因此,在下一步首先评估xs 以确定它与lengthwtilde 的第二种情况匹配,然后重复该过程。

将此与lengthwotilde 进行对比。在此函数中,对函数的第二种情况进行匹配的行为也会强制对参数进行评估。最终结果是相同的,但是能够更快地打开它而不是让另一个 thunk 被强制执行更有效。

从技术上讲,lengthwtilde 稍微复杂一些:参数已经在第二个分支中求值,因为这是我们确定我们所在的分支的方式,但是在传递到递归调用。

能够看到生产的核心非常有用。这是lengthwotilde 的输出(由ghc -O0 生成:

Foo.lengthwotilde =
  \ (@ t_afD)
    (@ a_afE)
    ($dNum_afF :: GHC.Num.Num a_afE)
    (eta_B1 :: [t_afD]) ->
    letrec {
      lengthwotilde1_af2 [Occ=LoopBreaker] :: [t_afD] -> a_afE
      [LclId, Arity=1]
      lengthwotilde1_af2 =
        \ (ds_dgd :: [t_afD]) ->
          case ds_dgd of _ {
            [] -> GHC.Num.fromInteger @ a_afE $dNum_afF (__integer 0);
            : ds1_dge xs_af1 ->
              GHC.Num.+
                @ a_afE
                $dNum_afF
                (GHC.Num.fromInteger @ a_afE $dNum_afF (__integer 1))
                (lengthwotilde1_af2 xs_af1)
          }; } in
    lengthwotilde1_af2 eta_B1

注意函数lengthwotilde1_af2 立即对参数ds_dgd(这是输入列表)执行case,然后在case 内递归,形成一个thunk(带有一些扩展):

1 + len [2..]
1 + (1 + len [3..])
1 + (1 + (1 + len [4..])

这最终需要评估 1 + (1 + (1 + (1 + ..)))

这里是lengthwtilde

Foo.lengthwtilde =
  \ (@ t_afW)
    (@ a_afX)
    ($dNum_afY :: GHC.Num.Num a_afX)
    (eta_B1 :: [t_afW]) ->
    letrec {
      lengthwtilde1_afM [Occ=LoopBreaker] :: [t_afW] -> a_afX
      [LclId, Arity=1]
      lengthwtilde1_afM =
        \ (ds_dgh :: [t_afW]) ->
          case ds_dgh of wild_X9 {
            [] -> GHC.Num.fromInteger @ a_afX $dNum_afY (__integer 0);
            : ipv_sgv ipv1_sgw ->
              GHC.Num.+
                @ a_afX
                $dNum_afY
                (GHC.Num.fromInteger @ a_afX $dNum_afY (__integer 1))
                (lengthwtilde1_afM
                   (case wild_X9 of _ {
                      [] ->
                        (Control.Exception.Base.irrefutPatError
                           @ () "foo.hs:(3,1)-(4,42)|(_ : xs)")
                        `cast` (UnsafeCo () [t_afW] :: () ~# [t_afW]);
                      : ds1_dgk xs_aeH -> xs_aeH
                    }))
          }; } in
    lengthwtilde1_afM eta_B1

对此的评估形成了不同的thunk:

len [1..]
1 + (len (if null [1..] then error else [2..]))
1 + (len [2..])
1 + (1 + len (if null [2..] then error else [3..]))

这最终会导致您第一次得到相同的添加链,但有一些额外的逻辑来处理无可辩驳的模式失败。

现在,如果您正在运行经过任何优化的编译代码,ghc 几乎肯定会发现参数不可能为 null,因为此时它们已经被评估并且已知使用 (:) 构造函数。当我用ghc -O2 编译代码并运行它时,两个函数的执行时间相同。它们都很糟糕,因为任何一种方式都会导致一连串的重击。 length 的标准定义要好得多,foldl' 定义也更好。

【讨论】:

    猜你喜欢
    • 2017-11-25
    • 2014-09-15
    • 1970-01-01
    • 2012-08-31
    • 2012-02-26
    • 2014-05-20
    • 2014-02-12
    • 2017-12-17
    • 1970-01-01
    相关资源
    最近更新 更多