【问题标题】:How to prevent common sub-expression elimination (CSE) with GHC如何使用 GHC 防止常见子表达式消除 (CSE)
【发布时间】:2011-05-07 09:24:20
【问题描述】:

给定程序:

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,但我更愿意修改库。

【问题讨论】:

  • 嗯,长镜头,但这提醒我,我想尝试以某种方式使assert magic 用户可见。如果事实证明这是可能的,它也可以充当一个标签,防止 GHC 对特定的函数调用进行 CSE 处理。
  • @Peter:我确实尝试过 assert 的东西,但这需要 -fno-ignore-asserts 才能工作。
  • (due to impurity in the library)。显然,由于这样做破坏了 Haskell 的语义,编译器会感到困惑。有没有办法以编译器可以理解的方式将代码重构为引用透明?例如。 ST monad 还是让它变得纯粹?
  • 你使用了 unsafePerformIO,你得到了你应得的。
  • @Don Stewart:这用于从用户代码中提取opt "example" 之类的注释(参见here)。由于用户代码几乎可以是任何东西,因此必须通过设计打破引用透明度。一个非常酷的 hack,但非常脆弱。

标签: optimization compiler-construction haskell ghc


【解决方案1】:

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

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)

虽然仍然是纯代码,但 GHC 不会尝试变得聪明,即使使用 -O2

    $ ./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。

【讨论】:

  • 不适合我的特定应用程序,但当然,这正是在几乎所有情况下都应该这样做的方式。对于实际代码,您可能应该使用putStrLn 而不是trace,并在IO monad 中返回runEval
  • 我不太确定拿起IO sledgehammer 显然是生产中的解决方案。我们从不过度排序的跟踪 monad 中获得了很好的优化好处,而且它是纯粹的,所以它更容易组合。我怀疑它有一些用途。
  • Neil 对 unsafePerformIO 的使用实际上是相当聪明的,而且根本不是性能关键(命令行参数解码)。我不知道有什么方法可以像使用 unsafePerformIO 那样方便。但我愿意为纯度付出不便的代价。
【解决方案2】:

读取 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。

【讨论】:

  • 回答您自己的问题?酷!
  • @augustss 事实上,我怀疑reverse "" 将在 CSE 更改之前落入构造函数专业化 - 我相信最终我会被编译器智取。
  • @Tener 我发布它时没有答案 - 我希望有人可能仍然有比我更好的答案,这对未来的 GHC 改进最有效。跨度>
  • 答案是不要使用 unsafePerformIO。您使用它的方式是错误的,因为您所做的不是以不纯的方式实现纯函数。你实际上是在使用副作用。这不是 Haskell 的用途。
【解决方案3】:

我认为您可以在源文件中指定-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,并且不再有公共子表达式。

【讨论】:

  • -fno-cse 在我的源文件中不起作用 - 它必须在使用我的库的人的源文件中,这使得它不太理想。没有实际的数据依赖关系,所以我不得不强制最终用户添加虚假的数据依赖关系,这使得库 API 非常难看。对于没有编写库并遇到此问题的任何人来说,这些都是有用的答案。
【解决方案4】:

我对 Don 的排序单子有点不确定(将其发布为答案,因为该网站不允许我添加 cmets)。稍微修改一下例子:

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 &lt;- ... 语句时触发第一个跟踪,而不是在return (t1 + t2) 中实际评估t1 时触发。如果我们将一元绑定运算符定义为

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 上测试)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-01-13
    • 1970-01-01
    • 2021-02-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多