Подтвердить что ты не робот

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

4b9b3361

Ответ 1

Чтение исходного кода в 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.

Ответ 2

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

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 для секвенирования была введена Саймон Марлоу.

Ответ 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 и больше не существует общего подвыражения.

Ответ 4

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

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), который является (ИМО) тем, что мы действительно хотим. Например, если мы изменим (t1 + t2) на (t2 + t1), это решение выдаст следующий результат:

hit 2
really hit 2
hit 1

Результат исходной версии остается неизменным, и мы не видим, когда наши термины действительно оцениваются:

hit 1
hit 2
really hit 2

Как и исходное решение, это также работает с -O3 (проверено на GHC 7.0.3).