我对Haskell很陌生,在阅读this和some performance tips on strictness后,我仍然想知道这对let
和where
表达式有何影响。如果我的代码如下:
f :: Int -> Int -> Int
f a b
|a==b = <simple computation>
|otherwise = e1 + 2 * e1 - e1^2
where e1 = <lengthy computation>
评估<lengthy computation>
的频率是多少?我假设如果e1
,在a==b
中给出Haskell的懒惰评估将不会被评估。但是,如果没有,e1
表达式中otherwise
被替换,然后在每次遇到它时进行评估,或者在第一次遇到它时进行评估,然后在所有后续事件中进行存储和重用?也:
-o
这样的标志?这与this question非常相似,但我找不到Haskell的答案。
非常感谢解释。
答案 0 :(得分:9)
作为一项规则,constant applicative form的a
或where
块中的代码仅被评估一次,并且仅在必要时(即,如果它根本没有使用,它根本就不会被评估。
let
不是一个恒定的应用形式,因为它有参数;它相当于
f
因此,每次使用两个参数调用函数时,f' = \a b -> let e1 = <lengthy computation>
in if a==b
then <simple computation>
else e1 + 2 * e1 - e1^2
都会被计算。这可能也是您想要的,事实上,如果e1
取决于<lengthy computation>
和a
,则可能采取的最佳行为。如果它只取决于b
,那么你可以做得更好:
a
当您执行此操作时,此表单会更有效f₂ a = \b ->
if a==b then <simple computation>
else e1 + 2 * e1 - e1^2
where e1 = <lengthy computation>
:在该示例中,map (f 34) [1,3,9,2,9]
只会为整个列表计算一次。 (但e1
在范围内没有<lengthy computation>
,因此无法依赖它。)
b
的情况。 (例如,如果它占用大量内存,而是快速来计算)。在这种情况下,你可以把它变成一个“无效函数”
e1
默认情况下,功能不会被记忆,因此在上文中,如果f₃ a b
| a==b = <simple computation>
| otherwise = e1() + 2 * e1() - e1()^2
where e1 () = <lengthy computation>
和其他三次,则<lengthy computation>
执行零次。
另一种可能性是强制a==b
始终只评估一次。您可以使用e1
:
seq
这是实际改变语义的唯一建议,而不仅仅是性能:假设我们始终定义f₄ a b = e1 `seq` if a==b
then <simple computation>
else e1 + 2 * e1 - e1^2
where e1 = <lengthy computation>
。然后,e1 = error "too tough"
,f
,f'
和f₂
仍然有效,前提是f₃
;但是a==b
在这种情况下甚至会失败。
对于优化(f₄
或-O
) - 这些通常不会改变程序的严格属性(即不能在{{{{{{{ 1}}和-O2
)。除此之外,编译器可以自由地进行任何它认为对性能有益的更改。但通常是,它不会改变我上面所说的内容。正如Taren评论的那样,主要的例外是f
:编译器将很容易内联f₄
然后共享对计算值的引用,这会阻止垃圾回收器回收内存。因此,最好不要依赖这种(无论如何有点hackish)技术。
答案 1 :(得分:4)
您可以查看GHC如何优化您的代码:
ghc -ddump-simpl -dsuppress-idinfo -dsuppress-coercions -dsuppress-type-applications -dsuppress-uniques -dsuppress-module-prefixes -fforce-recomp .\scratch.hs
这有点令人满意,所以你可能想要别名。这样的结果在很大程度上取决于优化级别,因此您可能希望对每个级别进行尝试。
g i = sum [1..i]
作为昂贵的计算和-O2我得到了这个输出:
==================== Tidy Core ====================
Result size of Tidy Core = {terms: 64, types: 23, coercions: 0}
Rec {
-- RHS size: {terms: 16, types: 3, coercions: 0}
$wgo :: Int# -> Int# -> Int#
$wgo =
\ (w :: Int#) (ww :: Int#) ->
case w of wild {
__DEFAULT -> $wgo (+# wild 1#) (+# ww wild);
10000# -> +# ww 10000#
}
end Rec }
-- RHS size: {terms: 15, types: 1, coercions: 0}
f2 :: Int
f2 =
case $wgo 1# 0# of ww { __DEFAULT ->
I# (-# (+# ww (*# 2# ww)) (*# ww ww))
}
-- RHS size: {terms: 2, types: 0, coercions: 0}
f1 :: Int
f1 = I# 42#
-- RHS size: {terms: 17, types: 8, coercions: 0}
f :: Int -> Int -> Int
f =
\ (a :: Int) (b :: Int) ->
case a of _ { I# x ->
case b of _ { I# y ->
case tagToEnum# (==# x y) of _ {
False -> f2;
True -> f1
}
}
}
与你的haskell版本相比,这是非常丑陋的,但有点眯眼它并不复杂。 $ wgo是我们昂贵的功能。这里有趣的部分是f1或f2(f的可能返回值)仅在第一次需要时计算一次。对于程序运行的其余部分,它们将被重用。