首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >为什么在模式匹配前添加一个波浪号会降低我的函数速度?

为什么在模式匹配前添加一个波浪号会降低我的函数速度?
EN

Stack Overflow用户
提问于 2013-03-03 09:52:44
回答 1查看 2.7K关注 0票数 21

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

代码语言:javascript
复制
lengthwtilde [] = 0
lengthwtilde ~(_:xs) = 1 + lengthwtilde xs

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

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

代码语言:javascript
复制
*Main> lengthwtilde [1..10000000]
10000000
(19.40 secs, 1731107132 bytes)
*Main> lengthwotilde [1..10000000]
10000000
(16.45 secs, 1531241716 bytes)

为什么会这样呢?

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2013-03-03 10:42:09

在模式匹配前面添加一个~会使该匹配变得无可辩驳。您可以将此视为向模式添加额外的惰性,以便它永远不会失败,除非该匹配绝对是评估所必需的。下面是一个简单的例子:

代码语言:javascript
复制
Prelude> (\ (_:_) -> "non-empty") []
"*** Exception: <interactive>:2:2-23: Non-exhaustive patterns in lambda

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

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

代码语言:javascript
复制
\ xs -> let (_:_) = xs in "oops"

正是这种懒惰的额外包装减慢了你的长度功能。如果将相同的let绑定转换应用于lengthwtilde,则会得到

代码语言:javascript
复制
lengthwtilde [] = 0
lengthwtilde xs' = let (_:xs) = xs' in 1 + lengthwtilde xs

想一想这是如何评估的。在顶层,您可以获得1+lengthwtilde xs。但是xs甚至没有被计算,因为它是一个let绑定的变量。因此,在下一步,评估第一个xs以确定它是否与第二个lengthwtilde匹配,并重复该过程。

这与lengthwotilde形成了对比。在此函数中,对函数的第二个案例进行匹配的操作也会强制对参数求值。最终的结果是相同的,但能够更快地解开它,而不是留下另一个强制的重击,效率更高。

从技术上讲,lengthwtilde稍微复杂一些:参数已经在第二个分支中进行了计算,因为这是我们确定所在分支的方式,但是当传递到递归调用中时,它会被重新包装。

能够看到生成的核心是很有用的。以下是lengthwotilde (由ghc -O0生成)的输出

代码语言:javascript
复制
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,然后在案例中递归,形成一个thunk (带有一些扩展):

代码语言:javascript
复制
1 + len [2..]
1 + (1 + len [3..])
1 + (1 + (1 + len [4..])

最终需要求值为1+ (1 + (1 + (1 +..)

这是lengthwtilde

代码语言:javascript
复制
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:

代码语言:javascript
复制
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编译并运行代码时,两个函数的执行时间相同。它们都很糟糕,因为任何一种方式都会导致一连串的故障。和一个好的foldl'定义一样,length的标准定义要好得多。

票数 43
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/15181610

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档