我在一段haskell代码中定义了两个函数:
lengthwtilde [] = 0
lengthwtilde ~(_:xs) = 1 + lengthwtilde xs
lengthwotilde [] = 0
lengthwotilde (_:xs) = 1 + lengthwotilde xs当我在ghci (使用:set +s)中测试它们时,我发现lengthwtilde (模式匹配前面有一个波浪号的那个)的执行速度比lengthwotilde慢了大约3秒。
*Main> lengthwtilde [1..10000000]
10000000
(19.40 secs, 1731107132 bytes)
*Main> lengthwotilde [1..10000000]
10000000
(16.45 secs, 1531241716 bytes)为什么会这样呢?
发布于 2013-03-03 10:42:09
在模式匹配前面添加一个~会使该匹配变得无可辩驳。您可以将此视为向模式添加额外的惰性,以便它永远不会失败,除非该匹配绝对是评估所必需的。下面是一个简单的例子:
Prelude> (\ (_:_) -> "non-empty") []
"*** Exception: <interactive>:2:2-23: Non-exhaustive patterns in lambda
Prelude> (\ ~(_:_) -> "oops") []
"oops"使用不可反驳的模式匹配,即使模式匹配在空列表上失败,因为没有计算绑定变量,所以没有错误。本质上,无可辩驳的模式匹配将函数转换为:
\ xs -> let (_:_) = xs in "oops"正是这种懒惰的额外包装减慢了你的长度功能。如果将相同的let绑定转换应用于lengthwtilde,则会得到
lengthwtilde [] = 0
lengthwtilde xs' = let (_:xs) = xs' in 1 + lengthwtilde xs想一想这是如何评估的。在顶层,您可以获得1+lengthwtilde xs。但是xs甚至没有被计算,因为它是一个let绑定的变量。因此,在下一步,评估第一个xs以确定它是否与第二个lengthwtilde匹配,并重复该过程。
这与lengthwotilde形成了对比。在此函数中,对函数的第二个案例进行匹配的操作也会强制对参数求值。最终的结果是相同的,但能够更快地解开它,而不是留下另一个强制的重击,效率更高。
从技术上讲,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,然后在案例中递归,形成一个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编译并运行代码时,两个函数的执行时间相同。它们都很糟糕,因为任何一种方式都会导致一连串的故障。和一个好的foldl'定义一样,length的标准定义要好得多。
https://stackoverflow.com/questions/15181610
复制相似问题