UIO works, and now includes a plugin to eliminate the calls to uniqueState

This commit is contained in:
Ryan Trinkle 2023-04-10 17:41:45 -04:00
parent 25cf31c7dd
commit f2ff3dbb09
3 changed files with 126 additions and 28 deletions

View File

@ -1,23 +1,55 @@
{-# LANGUAGE RecursiveDo #-} {-# LANGUAGE RecursiveDo #-}
{-# OPTIONS_GHC -fplugin UIO.Plugin #-}
module Test.UIO where module Test.UIO where
import Data.IORef import Data.IORef
import Control.Monad
import UIO import UIO
testBoth :: IO () testBoth :: IO ()
testBoth = do testBoth = do
putStrLn "testUIOFix"
testUIOFix testUIOFix
putStrLn "testUIOUnique"
testUIOUnique testUIOUnique
putStrLn "testUIOReplicate"
testUIOReplicate
putStrLn "testUIOBadTimeless"
testUIOBadTimeless
putStrLn "testUIOMany"
testUIOMany testUIOMany
putStrLn "testIO"
testIO testIO
{-# NOINLINE testUIOUnique #-}
testUIOUnique :: IO ()
testUIOUnique = do
r <- runUIO2 $ do
-- The following two lines can be merged by common subexpression elimination (CSE), which is very bad
r <- timeless $ newIORef 2
r2 <- timeless $ newIORef 2
-- Note that the following two lines must be different, otherwise they will *also* be merged by CSE, which will make the test appear to succeed!
unordered $ atomicModifyIORef' r $ \v -> (v + 5, ())
unordered $ atomicModifyIORef' r2 $ \v -> (v + 10, ())
pure r
print =<< readIORef r
{-# NOINLINE testUIOFix #-} {-# NOINLINE testUIOFix #-}
testUIOFix :: IO () testUIOFix :: IO ()
testUIOFix = do testUIOFix = do
r <- runUIO2 $ mdo r <- runUIO2 $ mdo
unordered $ atomicModifyIORef' r $ \v -> (v + 5, ()) unordered $ atomicModifyIORef' r $ \v -> (v + 5, ())
r <- unordered $ newIORef 2 r <- timeless $ newIORef 2
pure r
print =<< readIORef r
{-# NOINLINE testUIOBadTimeless #-}
testUIOBadTimeless :: IO ()
testUIOBadTimeless = do
r <- runUIO2 $ mdo
timeless $ atomicModifyIORef' r $ \v -> (v + 5, ())
r <- timeless $ newIORef 2
pure r pure r
print =<< readIORef r print =<< readIORef r
@ -25,7 +57,7 @@ testUIOFix = do
testUIOMany :: IO () testUIOMany :: IO ()
testUIOMany = do testUIOMany = do
r <- runUIO2 $ do r <- runUIO2 $ do
r <- unordered $ newIORef 0 r <- timeless $ newIORef 0
unordered $ atomicModifyIORef' r $ \v -> (v + 1, ()) unordered $ atomicModifyIORef' r $ \v -> (v + 1, ())
unordered $ atomicModifyIORef' r $ \v -> (v + 2, ()) unordered $ atomicModifyIORef' r $ \v -> (v + 2, ())
unordered $ atomicModifyIORef' r $ \v -> (v + 3, ()) unordered $ atomicModifyIORef' r $ \v -> (v + 3, ())
@ -39,18 +71,16 @@ testUIOMany = do
pure r pure r
print =<< readIORef r print =<< readIORef r
{-# NOINLINE testUIOUnique #-} {-# NOINLINE testUIOReplicate #-}
testUIOUnique :: IO () testUIOReplicate :: IO ()
testUIOUnique = do testUIOReplicate = do
r <- runUIO2 $ do rs <- runUIO2 $ do
-- The following two lines can be merged by common subexpression elimination (CSE), which is very bad rs <- replicateM 10 $ timeless $ newIORef 2
r <- unordered $ newIORef 2 forM_ rs $ \r ->
r2 <- unordered $ newIORef 2 unordered $ atomicModifyIORef' r $ \v -> (v + 5, ())
-- Note that the following two lines must be different, otherwise they will *also* be merged by CSE, which will make the test appear to succeed! pure rs
unordered $ atomicModifyIORef' r $ \v -> (v + 5, ()) vs <- mapM readIORef rs
unordered $ atomicModifyIORef' r2 $ \v -> (v + 10, ()) print $ sum vs
pure r
print =<< readIORef r
{-# NOINLINE testBoth #-} {-# NOINLINE testBoth #-}
testIO :: IO () testIO :: IO ()

View File

@ -29,23 +29,23 @@ import Data.List.NonEmpty (NonEmpty (..))
import Data.Semigroup import Data.Semigroup
import GHC.Magic import GHC.Magic
data Done = Done data RemainingWork = RemainingWork
instance Semigroup Done where instance Semigroup RemainingWork where
{-# INLINE (<>) #-} {-# INLINE (<>) #-}
(<>) = seq (<>) = seq
sconcat = mconcat . toList sconcat = mconcat . toList
stimes _ d = d stimes _ d = d
instance Monoid Done where instance Monoid RemainingWork where
{-# INLINE mempty #-} {-# INLINE mempty #-}
mempty = Done mempty = RemainingWork
mconcat = \case mconcat = \case
[] -> Done [] -> RemainingWork
x : xs -> x `seq` mconcat xs x : xs -> x `seq` mconcat xs
-- Unordered IO - we want to allocate things, strictly evaluate things, etc., but we don't actually care what order it is done in -- Unordered IO - we want to allocate things, strictly evaluate things, etc., but we don't actually care what order it is done in
newtype UIO2 a = UIO2 { unUIO2 :: State# RealWorld -> (Done, a) } newtype UIO2 a = UIO2 { unUIO2 :: State# RealWorld -> (RemainingWork, a) }
instance Functor UIO2 where instance Functor UIO2 where
{-# INLINE fmap #-} {-# INLINE fmap #-}
@ -55,15 +55,15 @@ instance Applicative UIO2 where
{-# INLINE pure #-} {-# INLINE pure #-}
{-# INLINE (*>) #-} {-# INLINE (*>) #-}
{-# INLINE (<*>) #-} {-# INLINE (<*>) #-}
pure x = UIO2 (\s -> (Done, x)) pure x = UIO2 (\s -> (RemainingWork, x))
UIO2 m *> UIO2 k = UIO2 (\s -> UIO2 m *> UIO2 k = UIO2 (\s ->
let (ms, _) = m (uniqueState 1# s) let (ms, _) = m (uniqueState 1# s)
(ks, b) = k (uniqueState 2# s) (ks, b) = k (uniqueState 2# s)
in (ms `seq` ks, b)) in (ms <> ks, b))
UIO2 m <*> UIO2 k = UIO2 (\s -> UIO2 m <*> UIO2 k = UIO2 (\s ->
let (ms, f) = m (uniqueState 1# s) let (ms, f) = m (uniqueState 1# s)
(ks, x) = k (uniqueState 2# s) (ks, x) = k (uniqueState 2# s)
in (ms `seq` ks, f x)) in (ms <> ks, f x))
instance Monad UIO2 where instance Monad UIO2 where
{-# INLINE (>>) #-} {-# INLINE (>>) #-}
@ -72,7 +72,7 @@ instance Monad UIO2 where
UIO2 m >>= k = UIO2 (\s -> UIO2 m >>= k = UIO2 (\s ->
let (ms, a) = m (uniqueState 1# s) let (ms, a) = m (uniqueState 1# s)
(ks, b) = unUIO2 (k a) (uniqueState 2# s) (ks, b) = unUIO2 (k a) (uniqueState 2# s)
in (ms `seq` ks, b)) in (ms <> ks, b))
instance MonadFix UIO2 where instance MonadFix UIO2 where
mfix k = UIO2 (\s -> mfix k = UIO2 (\s ->
@ -82,15 +82,26 @@ instance MonadFix UIO2 where
runUIO2 :: UIO2 a -> IO a runUIO2 :: UIO2 a -> IO a
runUIO2 (UIO2 m) = do runUIO2 (UIO2 m) = do
-- We use a bang pattern here instead of "evaluate", because "evaluate" leaves a "seq#" clutting up our core, but the bang pattern does not -- We use a bang pattern here instead of "evaluate", because "evaluate" leaves a "seq#" clutting up our core, but the bang pattern does not
(!Done, result) <- IO (\s -> (# s, m s #)) --TODO: This returns the same state we were given; should we call uniqueState 1 or something on it? (!RemainingWork, result) <- IO (\s -> (# s, m s #)) --TODO: This returns the same state we were given; should we call uniqueState 1 or something on it?
pure result pure result
-- The following is marked NOINLINE because unsafeDupablePerformIO is marked NOINLINE. I don't really understand it. -- The following is marked NOINLINE because unsafeDupablePerformIO is marked NOINLINE. I don't really understand it.
{-# INLINE unordered #-} {-# INLINE unordered #-}
unordered :: IO a -> UIO2 a unordered :: IO a -> UIO2 a
unordered (IO m) = UIO2 (\s -> case m s of (# _, x #) -> (Done, x)) unordered (IO m) = UIO2 (\s -> let x = case m s of (# _, x #) -> x in (x `seq` RemainingWork, x))
-- | Perform an action only when its result is needed. This action will be unique, but the computation will be considered finished regardless of whether this action has run. This is appropriate for functions like `newIORef`.
{-# INLINE timeless #-}
timeless :: IO a -> UIO2 a
timeless (IO m) = UIO2 (\s -> (RemainingWork, case m s of (# _, x #) -> x))
-- Force GHC to treat each of these state tokens as unique. This way, multiple identical calls, e.g. to newIORef are not treated as identical, because they have different state tokens. Ideally, we would inline this after common sub-expression elimination finishes, so that it is costless. -- Force GHC to treat each of these state tokens as unique. This way, multiple identical calls, e.g. to newIORef are not treated as identical, because they have different state tokens. Ideally, we would inline this after common sub-expression elimination finishes, so that it is costless.
{-# INLINE uniqueState #-} {-# NOINLINE uniqueState #-}
uniqueState :: Int# -> State# RealWorld -> State# RealWorld uniqueState :: Int# -> State# RealWorld -> State# RealWorld
uniqueState = noinline (\_ s -> s) uniqueState = uniqueState'
{-# NOINLINE uniqueState' #-}
uniqueState' :: Int# -> State# RealWorld -> State# RealWorld
uniqueState' _ s = s
-- This implementation seems to work sometimes, but I don't understand why, and it seems highly dependent on other aspects of the implementation.
-- uniqueState = runRW# (\s _ _ -> s)

57
src/UIO/Plugin.hs Normal file
View File

@ -0,0 +1,57 @@
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE LambdaCase #-}
module UIO.Plugin (plugin) where
import GHC.Plugins
import Data.Generics.Schemes (everywhereM)
import Data.Generics.Aliases (mkM)
import GHC.Types.Name
import Debug.Trace
isSpecificFunction :: Id -> Bool
isSpecificFunction f = case nameModule_maybe (idName f) of
Just mod -> moduleNameString (moduleName mod) == "UIO" && getOccString f == "uniqueState"
Nothing -> False
plugin :: Plugin
plugin = defaultPlugin
{ installCoreToDos = install
, pluginRecompile = purePlugin
}
install :: [CommandLineOption] -> [CoreToDo] -> CoreM [CoreToDo]
install _ todos = do
let myPass = CoreDoPluginPass ("Inline UIO.uniqueState") inlineSpecificFunction
return $ case insertAfterLastCSE myPass todos of
Nothing -> myPass : todos
Just newTodos -> newTodos
insertAfterLastCSE :: CoreToDo -> [CoreToDo] -> Maybe [CoreToDo]
insertAfterLastCSE myPass = fmap reverse . go . reverse
where go = \case
[] -> Nothing
passes@(CoreCSE {} : _) -> Just (myPass : passes)
passes@(h@(CoreDoPasses subPasses) : t) -> case insertAfterLastCSE myPass subPasses of
Nothing -> fmap (h :) $ insertAfterLastCSE myPass t
Just newSubPasses -> Just $ CoreDoPasses newSubPasses : t
h : t -> fmap (h :) $ insertAfterLastCSE myPass t
inlineSpecificFunction :: ModGuts -> CoreM ModGuts
inlineSpecificFunction guts = do
let !binds = mg_binds guts
inlinedBinds <- mapM (inlineSpecificFunctionBindM inlineSpecificFunctionTransform) binds
return guts { mg_binds = inlinedBinds }
inlineSpecificFunctionTransform :: CoreExpr -> CoreM CoreExpr
inlineSpecificFunctionTransform e@(App (App (Var f) _) s)
| isSpecificFunction f = return s
inlineSpecificFunctionTransform e = return e
inlineSpecificFunctionBindM :: (CoreExpr -> CoreM CoreExpr) -> CoreBind -> CoreM CoreBind
inlineSpecificFunctionBindM transform (NonRec b e) = do
e' <- everywhereM (mkM transform) e
return (NonRec b e')
inlineSpecificFunctionBindM transform (Rec pairs) = do
pairs' <- mapM (\(b, e) -> everywhereM (mkM transform) e >>= \e' -> return (b, e')) pairs
return (Rec pairs')