鉴于该计划:
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
,但我更愿意修改库。
答案 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
测试的表达式。目前,这意味着Expr
值Note
,Let
和Case
以及包含这些值的表达式。
因此,对上述问题的回答是:
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上测试)。