将`let`和`where`表达式的结果存储在haskell中吗?

时间:2017-05-02 12:16:50

标签: haskell evaluation

我对Haskell很陌生,在阅读thissome performance tips on strictness后,我仍然想知道这对letwhere表达式有何影响。如果我的代码如下:

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被替换,然后在每次遇到它时进行评估,或者在第一次遇到它时进行评估,然后在所有后续事件中进行存储和重用?也:

  • 有没有办法“手动”控制这个过程?
  • 这取决于天气我在ghci中运行代码还是用GHC编译它并且在GHC编译中它是否依赖于像-o这样的标志?

这与this question非常相似,但我找不到Haskell的答案。

非常感谢解释。

2 个答案:

答案 0 :(得分:9)

作为一项规则,constant applicative formawhere块中的代码仅被评估一次,并且仅在必要时(即,如果它根本没有使用,它根本就不会被评估。

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>,因此无法依赖它。)

OTOH,也可能存在您根本不需要
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"ff'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的可能返回值)仅在第一次需要时计算一次。对于程序运行的其余部分,它们将被重用。