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