Am I abusing unsafePerformIO?

前端 未结 4 1068
天命终不由人
天命终不由人 2020-12-28 14:25

To get acquainted with unsafePerformIO (how to use it and when to use it), I\'ve implemented a module for generating unique values.

Here\'s what I have:

4条回答
  •  攒了一身酷
    2020-12-28 14:57

    Yes, your module is dangerous. Consider this example:

    module Main where
    import Unique
    
    main = do
      print $ newUnique ()
      print $ newUnique ()
    

    Compile and run:

    $ ghc Main.hs
    $ ./Main
    U 0
    U 1
    

    Compile with optimization and run:

    $ \rm *.{hi,o}
    $ ghc -O Main.hs
    $ ./Main
    U 0
    U 0
    

    Uh-oh!

    Adding {-# NOINLINE counter #-} and {-# NOINLINE newUnique #-} does not help, so I'm not actually sure what's happening here ...

    1st UPDATE

    Looking at the GHC core, I see that @LambdaFairy was correct that constant subexpression elimination (CSE) caused my newUnique () expressions to be lifted. However, preventing CSE with -fno-cse and adding {-# NOINLINE counter #-} to Unique.hs is not sufficient to make the optimized program print the same as the unoptimized program! In particular, it seems that counter is inlined even with the NOINLINE pragma in Unique.hs. Does anyone understand why?

    I've uploaded the full versions of the following core files at https://gist.github.com/ntc2/6986500.

    The (relevant) core for main when compiling with -O:

    main3 :: Unique.Unique
    [GblId,
     Unf=Unf{Src=, 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=, 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=, 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=, 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
        }
    

    Note that the newUnique () calls have been lifted and bound to main3.

    And now when compiling with -O -fno-cse:

    main3 :: Unique.Unique
    [GblId,
     Unf=Unf{Src=, 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=, 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=, 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=, 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=, 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
        }
    

    Note that main3 and main5 are the two separate newUnique () calls.

    However:

    rm *.hi *o Main
    ghc -O -fno-cse Main.hs && ./Main
    U 0
    U 0
    

    Looking at the core for this modified 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'
    

    it seems that counter is being inlined as counter_rag, despite the NOINLINE pragma (2nd update: wrong! counter_rag is not marked with [InlPrag=NOINLINE], but that doesn't mean it's been inlined; rather, counter_rag is just the munged name of counter); the NOINLINE for newUnique is respected though:

    counter_rag :: IORef Type.Integer
    
    counter_rag =
      unsafeDupablePerformIO
        @ (IORef Type.Integer)
        (lvl1_rvg
         `cast` (Sym
                   (NTCo:IO )
                 :: (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 
                     :: 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 )
                 :: (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 }
    

    What's going on here?

    2nd UPDATE

    User @errge figured it out. Looking more carefully that the last core output pasted above, we see that most of the body of Unique.newUnique has been floated to the top level as lvl4_rvj. However, lvl4_rvj is a constant expression, not a function, and so it's only evaluated once, explaining the repeated U 0 output by main.

    Indeed:

    rm *.hi *o Main
    ghc -O -fno-cse -fno-full-laziness Main.hs && ./Main
    U 0
    U 1
    

    I don't understand exactly what the -ffull-laziness optimization does -- the GHC docs talk about floating let bindings, but the body of lvl4_rvj does not appear to have been a let binding -- but we can at least compare the above core with the core generated with -fno-full-laziness and see that now the body is not lifted:

    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` ( 
                           :: 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 )>
                   :: (State# RealWorld
                       -> (# State# RealWorld, Unique.Unique #))
                        ~#
                      IO Unique.Unique))
        }
    

    Here counter_rfj corresponds to counter again, and we see the difference is that the body of Unique.newUnique has not been lifted, and so the reference updating (readMutVar, writeMutVar) code will be run each time Unique.newUnique is called.

    I've updated the gist to include the new -fno-full-laziness core file. The earlier core files were generated on a different computer, so some minor differences here are unrelated to -fno-full-laziness.

提交回复
热议问题