Является ли эта реализация опасной? Может ли это привести GHC изменить семантику программы, которая ее использует?
Ответ 3
Да, ваш модуль опасен. Рассмотрим этот пример:
module Main where
import Unique
main = do
print $ newUnique ()
print $ newUnique ()
Скомпилировать и запустить:
$ ghc Main.hs
$ ./Main
U 0
U 1
Скомпилируйте с оптимизацией и запуском:
$ \rm *.{hi,o}
$ ghc -O Main.hs
$ ./Main
U 0
U 0
Э-э-о!
Добавление {-# NOINLINE counter #-}
и {-# NOINLINE newUnique #-}
не помогает, поэтому я не уверен, что происходит здесь...
1-е ОБНОВЛЕНИЕ
Глядя на ядро GHC, я вижу, что @LambdaFairy верна, что
постоянное исключение подвыражения (CSE) вызвало мой newUnique ()
выражения, которые нужно снять. Тем не менее, предотвращение CSE с -fno-cse
и
добавление {-# NOINLINE counter #-}
в Unique.hs
не является достаточным для
сделайте оптимизированную программу такой же, как и неоптимизированная программа!
В частности, кажется, что counter
встроен даже с
NOINLINE
прагма в Unique.hs
. Кто-нибудь понимает, почему?
Я загрузил полные версии следующих основных файлов на
https://gist.github.com/ntc2/6986500.
Ядро (реле) для main
при компиляции с -O
:
main3 :: Unique.Unique
[GblId,
Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=0, Value=False,
ConLike=False, Cheap=False, Expandable=False,
Guidance=IF_ARGS [] 20 0}]
main3 = Unique.newUnique ()
main2 :: [Char]
[GblId,
Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=0, Value=False,
ConLike=False, Cheap=False, Expandable=False,
Guidance=IF_ARGS [] 40 0}]
main2 =
Unique.$w$cshowsPrec 0 main3 ([] @ Char)
main4 :: [Char]
[GblId,
Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=0, Value=False,
ConLike=False, Cheap=False, Expandable=False,
Guidance=IF_ARGS [] 40 0}]
main4 =
Unique.$w$cshowsPrec 0 main3 ([] @ Char)
main1
:: State# RealWorld
-> (# State# RealWorld, () #)
[GblId,
Arity=1,
Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=1, Value=True,
ConLike=True, Cheap=True, Expandable=True,
Guidance=IF_ARGS [0] 110 0}]
main1 =
\ (eta_B1 :: State# RealWorld) ->
case Handle.Text.hPutStr2
Handle.FD.stdout main4 True eta_B1
of _ { (# new_s_atQ, _ #) ->
Handle.Text.hPutStr2
Handle.FD.stdout main2 True new_s_atQ
}
Обратите внимание, что вызовы newUnique ()
были сняты и привязаны к
main3
.
И теперь, когда компиляция с -O -fno-cse
:
main3 :: Unique.Unique
[GblId,
Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=0, Value=False,
ConLike=False, Cheap=False, Expandable=False,
Guidance=IF_ARGS [] 20 0}]
main3 = Unique.newUnique ()
main2 :: [Char]
[GblId,
Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=0, Value=False,
ConLike=False, Cheap=False, Expandable=False,
Guidance=IF_ARGS [] 40 0}]
main2 =
Unique.$w$cshowsPrec 0 main3 ([] @ Char)
main5 :: Unique.Unique
[GblId,
Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=0, Value=False,
ConLike=False, Cheap=False, Expandable=False,
Guidance=IF_ARGS [] 20 0}]
main5 = Unique.newUnique ()
main4 :: [Char]
[GblId,
Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=0, Value=False,
ConLike=False, Cheap=False, Expandable=False,
Guidance=IF_ARGS [] 40 0}]
main4 =
Unique.$w$cshowsPrec 0 main5 ([] @ Char)
main1
:: State# RealWorld
-> (# State# RealWorld, () #)
[GblId,
Arity=1,
Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=1, Value=True,
ConLike=True, Cheap=True, Expandable=True,
Guidance=IF_ARGS [0] 110 0}]
main1 =
\ (eta_B1 :: State# RealWorld) ->
case Handle.Text.hPutStr2
Handle.FD.stdout main4 True eta_B1
of _ { (# new_s_atV, _ #) ->
Handle.Text.hPutStr2
Handle.FD.stdout main2 True new_s_atV
}
Обратите внимание, что main3
и main5
являются двумя отдельными newUnique ()
вызовы.
Однако:
rm *.hi *o Main
ghc -O -fno-cse Main.hs && ./Main
U 0
U 0
Глядя на ядро для этого измененного Unique.hs
:
module Unique (newUnique) where
import Data.IORef
import System.IO.Unsafe (unsafePerformIO)
-- Type to represent a unique thing.
-- Show is derived just for testing purposes.
newtype Unique = U Integer
deriving Show
{-# NOINLINE counter #-}
counter :: IORef Integer
counter = unsafePerformIO $ newIORef 0
newUnique' :: IO Unique
newUnique' = do { x <- readIORef counter
; writeIORef counter (x+1)
; return $ U x }
{-# NOINLINE newUnique #-}
newUnique :: () -> Unique
newUnique () = unsafePerformIO newUnique'
кажется, что counter
вставляется как counter_rag
, несмотря на NOINLINE
pragma (второе обновление: wrong! counter_rag
не помечено [InlPrag=NOINLINE]
, но это doesn 't означает, что он был встроен, скорее, counter_rag
- это просто название munged counter
); NOINLINE
для newUnique
соблюдается, хотя:
counter_rag :: IORef Type.Integer
counter_rag =
unsafeDupablePerformIO
@ (IORef Type.Integer)
(lvl1_rvg
`cast` (Sym
(NTCo:IO <IORef Type.Integer>)
:: (State# RealWorld
-> (# State# RealWorld,
IORef Type.Integer #))
~#
IO (IORef Type.Integer)))
[...]
lvl3_rvi
:: State# RealWorld
-> (# State# RealWorld, Unique.Unique #)
[GblId, Arity=1]
lvl3_rvi =
\ (s_aqi :: State# RealWorld) ->
case noDuplicate# s_aqi of s'_aqj { __DEFAULT ->
case counter_rag
`cast` (NTCo:IORef <Type.Integer>
:: IORef Type.Integer
~#
STRef RealWorld Type.Integer)
of _ { STRef var#_au4 ->
case readMutVar#
@ RealWorld @ Type.Integer var#_au4 s'_aqj
of _ { (# new_s_atV, a_atW #) ->
case writeMutVar#
@ RealWorld
@ Type.Integer
var#_au4
(Type.plusInteger a_atW lvl2_rvh)
new_s_atV
of s2#_auo { __DEFAULT ->
(# s2#_auo,
a_atW
`cast` (Sym (Unique.NTCo:Unique)
:: Type.Integer ~# Unique.Unique) #)
}
}
}
}
lvl4_rvj :: Unique.Unique
lvl4_rvj =
unsafeDupablePerformIO
@ Unique.Unique
(lvl3_rvi
`cast` (Sym (NTCo:IO <Unique.Unique>)
:: (State# RealWorld
-> (# State# RealWorld, Unique.Unique #))
~#
IO Unique.Unique))
Unique.newUnique [InlPrag=NOINLINE] :: () -> Unique.Unique
Unique.newUnique =
\ (ds_dq8 :: ()) -> case ds_dq8 of _ { () -> lvl4_rvj }
Что здесь происходит?
2nd UPDATE
Пользователь @errge понял это.
Глядя более внимательно, что последний вывод ядра, вставленный выше, мы видим
что большая часть тела Unique.newUnique
была размещена на
верхний уровень как lvl4_rvj
. Однако lvl4_rvj
- постоянная
выражение, а не функция, и поэтому он оценивается только один раз,
объясняя повторный вывод U 0
на main
.
Действительно:
rm *.hi *o Main
ghc -O -fno-cse -fno-full-laziness Main.hs && ./Main
U 0
U 1
Я не понимаю точно, что оптимизация -ffull-laziness
делает -
Документы GHC
говорить о привязках с плавающей точкой, но тело lvl4_rvj
не
по-видимому, были связующим звеном - но мы можем по крайней мере сравнить вышеупомянутое ядро с
ядро, сгенерированное с помощью -fno-full-laziness
, и посмотрите, что теперь тело не поднимается:
Unique.newUnique [InlPrag=NOINLINE] :: () -> Unique.Unique
Unique.newUnique =
\ (ds_drR :: ()) ->
case ds_drR of _ { () ->
unsafeDupablePerformIO
@ Unique.Unique
((\ (s_as1 :: State# RealWorld) ->
case noDuplicate# s_as1 of s'_as2 { __DEFAULT ->
case counter_rfj
`cast` (<NTCo:IORef> <Type.Integer>
:: IORef Type.Integer
~#
STRef RealWorld Type.Integer)
of _ { STRef var#_avI ->
case readMutVar#
@ RealWorld @ Type.Integer var#_avI s'_as2
of _ { (# ipv_avz, ipv1_avA #) ->
case writeMutVar#
@ RealWorld
@ Type.Integer
var#_avI
(Type.plusInteger ipv1_avA (__integer 1))
ipv_avz
of s2#_aw2 { __DEFAULT ->
(# s2#_aw2,
ipv1_avA
`cast` (Sym <(Unique.NTCo:Unique)>
:: Type.Integer ~# Unique.Unique) #)
}
}
}
})
`cast` (Sym <(NTCo:IO <Unique.Unique>)>
:: (State# RealWorld
-> (# State# RealWorld, Unique.Unique #))
~#
IO Unique.Unique))
}
Здесь counter_rfj
снова соответствует counter
, и мы видим, что
разница в том, что тело Unique.newUnique
не было снято,
и поэтому код ссылки (readMutVar
, writeMutVar
) будет
запускается каждый раз, когда вызывается Unique.newUnique
.
Я обновил смысл до
включить новый файл -fno-full-laziness
. Раннее ядро
файлы были сгенерированы на другом компьютере, поэтому некоторые незначительные
различия здесь не связаны с -fno-full-laziness
.