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

Nei*_*ell 25 compiler-construction optimization haskell ghc

鉴于该计划:

import Debug.Trace
main = print $ trace "hit" 1 + trace "hit" 1
Run Code Online (Sandbox Code Playgroud)

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

hit
2
Run Code Online (Sandbox Code Playgroud)

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

main = print $ let x = trace "hit" 1 in x + x
Run Code Online (Sandbox Code Playgroud)

如果我编译-fno-cse然后我看到hit出现两次.

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

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

Don*_*art 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
Run Code Online (Sandbox Code Playgroud)

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

main = print $ runEval $ do
            t1 <- track "hit" 1
            t2 <- track "hit" 1
            return (t1 + t2)
Run Code Online (Sandbox Code Playgroud)

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

    $ ./A
    hit
    hit
    2
Run Code Online (Sandbox Code Playgroud)

因此,我们只引入足以向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"
Run Code Online (Sandbox Code Playgroud)

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


参考文献:Simon MarlowEval介绍了有用的测序monad .

  • 我不太确定拿起“IO”大锤显然是生产中的解决方案。我们从不过度排序的跟踪 monad 中获得了很好的优化优势,而且它是纯的,因此更容易组合。我怀疑它有一些用途。 (2认同)
  • Neil使用unsafePerformIO实际上相当聪明,而且根本不是性能关键(命令行参数解码).我不知道有什么方法可以像使用unsafePerformIO一样方便.但我愿意为纯度带来不便. (2认同)

Nei*_*ell 12

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

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

unit = reverse "" `seq` ()

main = print $ trace "hit" (case unit of () -> 1) +
               trace "hit" (case unit of () -> 1)
Run Code Online (Sandbox Code Playgroud)

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

  • 答案是不要使用unsafePerformIO.你使用它的方式是错误的,因为你所做的并不是以不纯的方式实现纯函数.你实际上是在使用副作用.这不是Haskell的用途. (12认同)

Hei*_*mus 8

我想你可以-fno-cse在源文件中指定选项,即通过放置一个pragma

{-# OPTIONS_GHC -fno-cse #-}
Run Code Online (Sandbox Code Playgroud)

在上面.


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

let x () = trace "hi" 1 in x () + x ()
Run Code Online (Sandbox Code Playgroud)

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

let
    x dummy = trace "hi" $ dummy `seq` 1
    x1      = x ()
    x2      = x x1 
in x1 + x2
Run Code Online (Sandbox Code Playgroud)

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

  • 我的源文件中的-fno-cse将无法正常工作 - 它必须位于使用我的库的人的源文件中,这使得它不太理想.没有实际的数据依赖性,所以我强迫最终用户添加假数据依赖,使库API真的很难看.对于没有编写库并且遇到此问题的任何人来说,这些都是有用的答案. (2认同)