簡體   English   中英

我濫用不安全的PerformIO嗎?

[英]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
    }

請注意, main3main5是兩個單獨的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名字); NOINLINEnewUniqueNOINLINE 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都會運行引用更新( readMutVarwriteMutVar )代碼。

我已經更新了要點 ,包括新的-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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM