[英]Am I abusing unsafePerformIO?
為了熟悉unsafePerformIO
(如何使用它以及何時使用它),我實現了一個用於生成唯一值的模塊。
這就是我所擁有的:
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
-- I believe this is the Haskell'98 derived instance, but
-- I want to be explicit, since its Eq instance is the most
-- important part of Unique.
instance Eq Unique where
(U x) == (U y) = x == y
counter :: IORef Integer
counter = unsafePerformIO $ newIORef 0
updateCounter :: IO ()
updateCounter = do
x <- readIORef counter
writeIORef counter (x+1)
readCounter :: IO Integer
readCounter = readIORef counter
newUnique' :: IO Unique
newUnique' = do { x <- readIORef counter
; writeIORef counter (x+1)
; return $ U x }
newUnique :: () -> Unique
newUnique () = unsafePerformIO newUnique'
令我高興的是,名為Data.Unique
的包選擇了與我相同的數據類型; 另一方面,他們選擇了newUnique :: IO Unique
類型,但是如果可能的話我想要遠離IO
。
這種實施是危險的嗎? 是否可能導致GHC改變使用它的程序的語義?
將unsafePerformIO
視為對編譯器的承諾。 它說:“我保證你可以把這個IO動作視為純粹的價值而不會出錯”。 它很有用,因為有時你可以為使用不純操作實現的計算構建一個純接口,但編譯器不可能驗證何時是這種情況; 相反, unsafePerformIO
允許你把手放在心上,並發誓你已經證實不純的計算實際上是純粹的,所以編譯器可以簡單地相信它是。
在這種情況下,承諾是錯誤的。 如果newUnique
是一個純函數,那么let x = newUnique () in (x, x)
和(newUnique (), newUnique ())
將是等價的表達式。 但是你希望這兩個表達式有不同的結果; 在一種情況下具有相同Unique
值的一對副本,在另一種情況下具有一對兩個不同的Unique
值。 使用您的代碼,實際上沒有辦法說出這兩個表達式的含義。 它們只能通過考慮程序在運行時執行的實際操作順序來理解,並且當您使用unsafePerformIO
時,對此的控制正是您放棄的。 unsafePerformIO
表示, 無論是將一個表達式編譯為newUnique
還是兩個執行都newUnique
,並且Haskell的任何實現都可以隨意選擇它們每次遇到這樣的代碼。
unsafePerformIO
的目的是當你的函數在內部執行某些操作時,但沒有觀察者會注意到的副作用。
例如,一個帶矢量,復制它,就地復制快速排序,然后返回副本的函數。
(參見注釋)這些操作中的每一個都有副作用, IO
也是如此,但總體結果卻沒有。
newUnique
必須是一個IO
動作,因為它每次都會產生不同的東西。 這基本上是IO
的定義,它意味着動詞 ,而不是形容詞的功能。 函數將始終為相同的參數返回相同的結果。 這稱為參照透明度。
有關unsafePerformIO
有效用法,請參閱此問題 。
是的,你的模塊很危險。 考慮這個例子:
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 #-}
沒有用,所以我真的不確定這里發生了什么......
看看GHC核心,我看到@LambdaFairy是正確的,因為常量子表達式消除(CSE)導致我的newUnique ()
表達式被解除。 但是,使用-fno-cse
{-# NOINLINE counter #-}
阻止CSE並將{-# NOINLINE counter #-}
到Unique.hs
不足以使優化程序打印與未優化程序相同! 特別是,它似乎 。 有誰理解為什么? counter
與偶數內聯NOINLINE
編譯中Unique.hs
我已經在https://gist.github.com/ntc2/6986500上傳了以下核心文件的完整版本。
使用-O
編譯時main
的(相關)核心:
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'
盡管有 (第二次更新:錯誤! NOINLINE
編譯指示,似乎counter
被內聯為counter_rag
counter_rag
沒有用[InlPrag=NOINLINE]
標記,但這並不意味着它已被內聯;相反, counter_rag
只是counter
名字); NOINLINE
, newUnique
的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 }
這里發生了什么?
用戶@errge 想出來了 。 仔細觀察上面粘貼的最后一個核心輸出,我們看到Unique.newUnique
大部分主體已經作為lvl4_rvj
浮動到頂層。 但是, lvl4_rvj
是一個常量表達式 ,而不是一個函數,所以它只被評估一次,解釋了main
的重復U 0
輸出。
確實:
rm *.hi *o Main
ghc -O -fno-cse -fno-full-laziness Main.hs && ./Main
U 0
U 1
我並不完全理解-ffull-laziness
優化的作用 - GHC文檔談論浮動let綁定,但lvl4_rvj
的主體似乎並不是一個let綁定 - 但我們至少可以比較上面的核心用-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
的主體沒有被提升,因此每次調用Unique.newUnique
都會運行引用更新( readMutVar
, writeMutVar
)代碼。
我已經更新了要點 ,包括新的-fno-full-laziness
核心文件。 早期的核心文件是在另一台計算機上生成的,因此這里的一些細微差別與-fno-full-laziness
無關。
查看另一個示例如何失敗:
module Main where
import Unique
helper :: Int -> Unique
-- noinline pragma here doesn't matter
helper x = newUnique ()
main = do
print $ helper 3
print $ helper 4
使用此代碼時,效果與ntc2的示例相同:使用-O0更正,但使用-O更正。 但是在這段代碼中沒有“消除共同的子表達式”。
這里實際發生的是newUnique ()
表達式“浮出”到頂層,因為它不依賴於函數的參數。 在GHC中,這是-ffull-laziness
(默認情況下使用-O
,可以使用-O -fno-full-laziness
關閉)。
所以代碼有效地變成了這樣:
helperworker = newUnique ()
helper x = helperworker
在這里,helperworker是一個只能評估一次的thunk。
使用已推薦的NOINLINE編譯指示,如果在命令行中添加-fno-full-laziness
,則它會按預期工作。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.