我在一段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)
为什么会这样?
答案 0 :(得分:37)
在模式匹配前添加~
会使匹配无可辩驳。您可以将此视为向模式添加额外的延迟,以便它永远不会匹配,除非评估绝对需要匹配。这是一个简单的例子:
Prelude> (\ (_:_) -> "non-empty") []
"*** Exception: <interactive>:2:2-23: Non-exhaustive patterns in lambda
Prelude> (\ ~(_:_) -> "oops") []
"oops"
使用无可辩驳的模式匹配,即使模式匹配在空列表上失败,由于没有计算绑定变量,因此没有错误。基本上,无可辩驳的模式匹配将函数转换为:
\ xs -> let (_:_) = xs in "oops"
这是懒惰的额外包裹,会减慢你的长度功能。如果您将相同的let-binding转换应用于lengthwtilde
,则
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
立即对参数case
执行ds_dgd
(这是输入列表),然后在案例内部进行递归,形成一个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
对此形成不同的评价:
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'
定义很好。