Как предотвратить удаление общего подвыражения (CSE) с помощью 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'd? Я знаю о lazy, но не могу найти ничего, предназначенного для подавления CSE.

Предпосылкой этого вопроса является библиотека cmdargs, в которой CSE разбивает библиотеку (из-за примесей в библиотеке). Одним из решений является попросить пользователей библиотеки указать -fno-cse, но я бы предпочел изменить библиотеку.

4 ответа

Решение

Считывая исходный код в 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 не может trace функция и это 2 аргумента, и мы получаем hit напечатано дважды. Это работает как с GHC 6.12.4, так и с 7.0.3 в -O2,

Как насчет удаления источника проблемы - неявного эффекта - с помощью монады секвенирования, которая вводит этот эффект? Например, монада строгой идентификации с отслеживанием:

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 сделал все возможное для оптимизации кода, включая статическое вычисление математики, сохранив при этом правильную трассировку.


Рекомендации: полезные Eval монада для секвенирования была введена Саймоном Марлоу.

Я думаю, что вы можете указать -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 и больше нет общего подвыражения.

Я немного не уверен насчет монады секвенирования Дона (публикуя это как ответ, потому что сайт не позволяет мне добавлять комментарии). Немного изменив пример:

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), Если мы определим оператор монадического связывания как

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).

Другие вопросы по тегам