如何使用GHC防止常见的子表达式消除(CSE)

时间:2011-05-07 09:24:20

标签: optimization compiler-construction haskell ghc

鉴于该计划:

import Debug.Trace
main = print $ trace "hit" 1 + trace "hit" 1

如果我使用ghc -O(7.0.1或更高版本)编译,我会得到输出:

hit
2

即。 GHC使用常见的子表达式消除(CSE)将我的程序重写为:

main = print $ let x = trace "hit" 1 in x + x

如果我使用-fno-cse进行编译,那么我会看到hit出现两次。

是否可以通过修改程序来避免CSE?是否有任何子表达式e,我可以保证e + e不会是CSE的?我知道lazy,但找不到任何可以抑制CSE的东西。

这个问题的背景是cmdargs库,其中CSE打破了库(由于库中的杂质)。一种解决方案是要求库的用户指定-fno-cse,但我更愿意修改库。

4 个答案:

答案 0 :(得分:28)

如何通过使用引入该效果的测序monad来消除问题的根源 - 隐含的效果?例如。严格的身份monad跟踪:

data Eval a = Done a
            | Trace String a

instance Monad Eval where
  return x = Done x

  Done x    >>= k = k x
  Trace s a >>= k = trace s (k a)

runEval :: Eval a -> a
runEval (Done x) = x

track = Trace

现在我们可以编写保证trace调用顺序的内容:

main = print $ runEval $ do
            t1 <- track "hit" 1
            t2 <- track "hit" 1
            return (t1 + t2)

虽然仍然是纯粹的代码,但即使使用-O2,GHC也不会试图变得聪明:

    $ ./A
    hit
    hit
    2

所以我们只引入足以向GHC传授我们想要的语义的计算效果(跟踪)。

这对于编译优化来说非常非常强大。因此,GHC在编译时将数学优化为2,但仍保留trace语句的顺序。


作为这种方法有多强大的证据,这里是-O2和积极内联的核心:

main2 =
  case Debug.Trace.trace string trace2 of
    Done x -> case x of 
        I# i# -> $wshowSignedInt 0 i# []
    Trace _ _ -> err

trace2 = Debug.Trace.trace string d

d :: Eval Int
d = Done n

n :: Int
n = I# 2

string :: [Char]
string = unpackCString# "hit"

所以GHC已尽其所能优化代码 - 包括静态计算数学 - 同时仍然保留正确的跟踪。


参考文献Simon Marlow引入了有用的Eval测序monad。

答案 1 :(得分:12)

将源代码读取到GHC,唯一不符合CSE条件的表达式是那些未通过exprIsBig测试的表达式。目前,这意味着ExprNoteLetCase以及包含这些值的表达式。

因此,对上述问题的回答是:

unit = reverse "" `seq` ()

main = print $ trace "hit" (case unit of () -> 1) +
               trace "hit" (case unit of () -> 1)

这里我们创建一个值unit,它解析为(),但哪个GHC无法确定其值(通过使用递归函数GHC无法优化 - reverse只是一个简单的手。)这意味着GHC无法CSE trace函数及其2个参数,我们将hit打印两次。这适用于-O2的GHC 6.12.4和7.0.3。

答案 2 :(得分:8)

我认为您可以在源文件中指定-fno-cse选项,即通过放置编译指示

{-# OPTIONS_GHC -fno-cse #-}

在上面。


另一种避免常见子表达式消除或一般浮动的方法是引入伪参数。例如,您可以尝试

let x () = trace "hi" 1 in x () + x ()

这个特殊的例子不一定有效;理想情况下,您应该通过伪参数指定数据依赖关系。例如,以下内容可能有效:

let
    x dummy = trace "hi" $ dummy `seq` 1
    x1      = x ()
    x2      = x x1 
in x1 + x2

x现在“的结果”取决于参数dummy,并且不再存在共同的子表达式。

答案 3 :(得分:4)

我对Don的测序monad有点不确定(将此作为答案发布,因为该网站不允许我添加评论)。稍微修改一下示例:

main :: IO ()
main = print $ runEval $ do
            t1 <- track "hit 1" (trace "really hit 1" 1)
            t2 <- track "hit 2" 2
            return (t1 + t2)

这给了我们以下输出:

hit 1
hit 2
really hit 1

也就是说,第一个跟踪在执行t1 <- ...语句时触发,而不是在t1中实际评估return (t1 + t2)时触发。如果我们将monadic绑定运算符定义为

Done x    >>= k = k x
Trace s a >>= k = k (trace s a)

相反,输出将反映实际的评估顺序:

hit 1
really hit 1
hit 2

也就是说,执行(t1 + t2)语句时会触发跟踪,这是(IMO)我们真正想要的。例如,如果我们将(t1 + t2)更改为(t2 + t1),则此解决方案会生成以下输出:

hit 2
really hit 2
hit 1

原始版本的输出保持不变,我们看不到我们的术语何时被真正评估:

hit 1
hit 2
really hit 2

与原始解决方案一样,这也适用于-O3(在GHC 7.0.3上测试)。