`let` 和 `where` 表达式的结果会存储在 haskell 中吗?
will the result of `let` and `where` expressions be stored in haskell?
我对 Haskell 比较陌生,在阅读 this and 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>
多久评估一次?我假设给定 Haskell 在 e1
中的惰性求值如果 a==b
根本不会被求值。但如果不是,是 e1
被替换到 otherwise
表达式中,然后每次遇到它时都求值,还是在第一次遇到它时求值,然后存储并在所有后续出现时重复使用?还有:
- 有没有办法控制这个过程"manually"?
- 这是否取决于天气我 运行 在 ghci 中编写代码或使用 GHC 编译它,在 GHC 编译中它是否取决于像
-o
这样的标志?
这与 非常相似,但我找不到 Haskell 的答案。
非常感谢解释。
通常,constant applicative form 的 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
,所以它 不能 依赖于它。)
OTOH,也可能存在您根本不想 e1
保留的情况。 (例如,如果它占用大量内存,但计算起来 quick)。在这种情况下,您可以将其设为“零函数”
f₃ a b
| a==b = <simple computation>
| otherwise = e1() + 2 * e1() - e1()^2
where e1 () = <lengthy computation>
默认情况下不记忆函数,因此在上面,如果 a==b
则 <lengthy computation>
执行零次,否则执行三次。
还有另一种可能性是强制 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
或 -O2
)——这些通常不会改变程序的严格属性(即不能在 [=16= 的行为之间改变) ] 和 f₄
)。除此之外,编译器几乎可以自由地进行它认为对性能有益的任何更改。但是通常,它不会改变我上面说的任何东西。正如 Taren 所说,主要的例外是类似于 f₃
:编译器将很容易地内联 e1 ()
,然后共享对计算值的引用,这会阻止垃圾收集器回收内存。所以最好不要依赖这种(无论如何有点老套)技术。
您可以实际查看 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 的可能 return 值,仅在第一次需要时才计算一次。对于程序的其余部分 运行,它们被重复使用。
我对 Haskell 比较陌生,在阅读 this and 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>
多久评估一次?我假设给定 Haskell 在 e1
中的惰性求值如果 a==b
根本不会被求值。但如果不是,是 e1
被替换到 otherwise
表达式中,然后每次遇到它时都求值,还是在第一次遇到它时求值,然后存储并在所有后续出现时重复使用?还有:
- 有没有办法控制这个过程"manually"?
- 这是否取决于天气我 运行 在 ghci 中编写代码或使用 GHC 编译它,在 GHC 编译中它是否取决于像
-o
这样的标志?
这与
非常感谢解释。
通常,constant applicative form 的 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
,所以它 不能 依赖于它。)
OTOH,也可能存在您根本不想 e1
保留的情况。 (例如,如果它占用大量内存,但计算起来 quick)。在这种情况下,您可以将其设为“零函数”
f₃ a b
| a==b = <simple computation>
| otherwise = e1() + 2 * e1() - e1()^2
where e1 () = <lengthy computation>
默认情况下不记忆函数,因此在上面,如果 a==b
则 <lengthy computation>
执行零次,否则执行三次。
还有另一种可能性是强制 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
或 -O2
)——这些通常不会改变程序的严格属性(即不能在 [=16= 的行为之间改变) ] 和 f₄
)。除此之外,编译器几乎可以自由地进行它认为对性能有益的任何更改。但是通常,它不会改变我上面说的任何东西。正如 Taren 所说,主要的例外是类似于 f₃
:编译器将很容易地内联 e1 ()
,然后共享对计算值的引用,这会阻止垃圾收集器回收内存。所以最好不要依赖这种(无论如何有点老套)技术。
您可以实际查看 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 的可能 return 值,仅在第一次需要时才计算一次。对于程序的其余部分 运行,它们被重复使用。