futhark-0.7.3: src/Futhark/Optimise/MemoryBlockMerging/Liveness/LastUse.hs
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ConstraintKinds #-}
-- | Find last uses for all memory blocks.
--
-- A memory block can have more than one last use.
module Futhark.Optimise.MemoryBlockMerging.Liveness.LastUse
( findLastUses
) where
import qualified Data.Set as S
import qualified Data.Map.Strict as M
import Control.Monad
import Control.Monad.RWS
import Futhark.Representation.AST
import Futhark.Representation.ExplicitMemory
(ExplicitMemorish, ExplicitMemory)
import qualified Futhark.Representation.ExplicitMemory as ExpMem
import Futhark.Representation.Kernels.Kernel
import Futhark.Optimise.MemoryBlockMerging.Miscellaneous
import Futhark.Optimise.MemoryBlockMerging.Types
type LastUsesList = [LastUses]
getLastUsesMap :: LastUsesList -> LastUses
getLastUsesMap = M.unionsWith S.union
-- Mapping from a memory block to its currently assumed last use statement
-- variable.
type OptimisticLastUses = M.Map VName (StmOrRes, Bool)
data Context = Context
{ ctxVarToMem :: VarMemMappings MemorySrc
, ctxMemAliases :: MemAliases
, ctxFirstUses :: FirstUses
, ctxExistentials :: Names
, ctxCurFirstUsesOuter :: Names
}
deriving (Show)
data Current = Current
{ curOptimisticLastUses :: OptimisticLastUses
, curFirstUses :: Names
}
deriving (Show)
newtype FindM lore a = FindM { unFindM :: RWS Context LastUsesList Current a }
deriving (Monad, Functor, Applicative,
MonadReader Context,
MonadWriter LastUsesList,
MonadState Current)
type LoreConstraints lore = (ExplicitMemorish lore,
FullWalk lore)
coerce :: FindM flore a -> FindM tlore a
coerce = FindM . unFindM
-- Find the memory blocks used or aliased by a variable.
varMems :: VName -> FindM lore MNames
varMems var =
maybe S.empty (S.singleton . memSrcName) <$> asks (M.lookup var . ctxVarToMem)
modifyCurOptimisticLastUses :: (OptimisticLastUses -> OptimisticLastUses) -> FindM lore ()
modifyCurOptimisticLastUses f =
modify $ \c -> c { curOptimisticLastUses = f $ curOptimisticLastUses c }
modifyCurFirstUses :: (Names -> Names) -> FindM lore ()
modifyCurFirstUses f = modify $ \c -> c { curFirstUses = f $ curFirstUses c }
withLocalCurFirstUses :: FindM lore a -> FindM lore a
withLocalCurFirstUses m = do
cur_first_uses <- gets curFirstUses
res <- m
modifyCurFirstUses $ const cur_first_uses
return res
recordMapping :: StmOrRes -> MName -> FindM lore ()
recordMapping var mem = tell [M.singleton var (S.singleton mem)]
-- | Find all last uses of *memory blocks* in a function definition.
findLastUses :: VarMemMappings MemorySrc -> MemAliases -> FirstUses -> Names
-> FunDef ExplicitMemory -> LastUses
findLastUses var_to_mem mem_aliases first_uses existentials fundef =
let context = Context
{ ctxVarToMem = var_to_mem
, ctxMemAliases = mem_aliases
, ctxFirstUses = first_uses
, ctxExistentials = existentials
, ctxCurFirstUsesOuter = S.empty
}
m = unFindM $ do
forM_ (funDefParams fundef) lookInFunDefFParam
lookInBody $ funDefBody fundef
mapM_ lookInRes $ bodyResult $ funDefBody fundef
optimistics <- gets curOptimisticLastUses
forM_ (M.keys optimistics) $ \mem ->
commitOptimistic mem
last_uses = removeEmptyMaps $ getLastUsesMap
$ snd $ evalRWS m context (Current M.empty S.empty)
in last_uses
-- Optimistically say that the last use of 'mem' and all its memory aliases is
-- at 'x_lu'. Exclude 'exclude' from the memory aliases (necessary in a few
-- edge cases).
setOptimistic :: MName -> StmOrRes -> MNames -> FindM lore ()
setOptimistic mem x_lu exclude = do
-- Will override any previous optimistic last use.
mem_aliases <- asks ctxMemAliases
let mems = S.difference (S.union (S.singleton mem)
$ lookupEmptyable mem mem_aliases) exclude
forM_ mems $ \mem' -> do
let is_indirect = mem' /= mem
modifyCurOptimisticLastUses $ M.insert mem' (x_lu, is_indirect)
-- If an optimistic last use 'mem' was added through a memory alias, forget
-- about it.
removeIndirectOptimistic :: MName -> FindM lore ()
removeIndirectOptimistic mem = do
res <- M.lookup mem <$> gets curOptimisticLastUses
case res of
Just (_, True) -> -- Means that is was added indirectly.
modifyCurOptimisticLastUses $ M.delete mem
_ -> return ()
-- Set the optimistic last use in stone.
commitOptimistic :: MName -> FindM lore ()
commitOptimistic mem = do
res <- M.lookup mem <$> gets curOptimisticLastUses
case res of
Just (x_lu, _) -> recordMapping x_lu mem
Nothing -> return ()
lookInFunDefFParam :: FParam lore -> FindM lore ()
lookInFunDefFParam (Param x _) = do
first_uses_x <- lookupEmptyable x <$> asks ctxFirstUses
modifyCurFirstUses $ S.union first_uses_x
lookInBody :: LoreConstraints lore =>
Body lore -> FindM lore ()
lookInBody (Body _ bnds _res) =
mapM_ lookInStm bnds
lookInKernelBody :: LoreConstraints lore =>
KernelBody lore -> FindM lore ()
lookInKernelBody (KernelBody _ bnds _res) =
mapM_ lookInStm bnds
lookInStm :: LoreConstraints lore =>
Stm lore -> FindM lore ()
lookInStm (Let (Pattern _patctxelems patvalelems) _ e) = do
-- When an loop, a scan, a reduce, or a stream contains a use of an array that
-- is created before the expression body, it should not get a last use in a
-- statement inside the inner body, since loops can have cycles, and so its
-- proper last use should really be in the statement declaring the sub-body,
-- and not in some statement in the sub-body. See
-- 'tests/reuse/loop/copy-from-outside.fut for an example of this.
cur_first_uses <- gets curFirstUses
let mMod = case e of
If{} -> id -- If is the only other expression with a body.
_ -> local $ \ctx -> ctx { ctxCurFirstUsesOuter = cur_first_uses }
-- First handle all pattern elements by themselves.
forM_ patvalelems $ \(PatElem x membound) ->
case membound of
ExpMem.MemArray _ _ _ (ExpMem.ArrayIn xmem _) -> do
first_uses_x <- lookupEmptyable x <$> asks ctxFirstUses
modifyCurFirstUses $ S.union first_uses_x
-- When this is a new first use of a memory block, commit the previous
-- optimistic last use of it, so that it can be considered unused in
-- the statements inbetween.
when (S.member xmem first_uses_x) $ commitOptimistic xmem
_ -> return ()
-- Then find the new memory blocks.
let e_free_vars = freeInExp e `S.difference` S.fromList (freeExcludes e)
e_mems <- S.unions <$> mapM varMems (S.toList e_free_vars)
mem_aliases <- asks ctxMemAliases
first_uses_outer <- asks ctxCurFirstUsesOuter
-- Then handle the pattern elements by themselves again.
forM_ patvalelems $ \(PatElem x _) ->
-- Set all memory blocks being used as optimistic last uses.
forM_ (S.toList e_mems) $ \mem -> do
-- If the memory has its first use outside the current body, it is
-- dangerous to set its last use to be in a statement inside the body,
-- since the body can be run multiple times in cases of loops or kernels,
-- so we only set the last use of a memory to this statement if it also
-- has its first use inside the current body.
--
-- If it (or any aliased memory) does have its first use outside the body,
-- we remove any existing optimistic last use, although only if such an
-- optimistic last use was added as a side effect of adding an existential
-- optimistic last use (i.e. it was aliased by the existential memory
-- which had a last use).
let from_outer = any (`S.member` first_uses_outer)
(mem : S.toList (lookupEmptyable mem mem_aliases))
if from_outer
then removeIndirectOptimistic mem
else setOptimistic mem (FromStm x) S.empty
if S.null (lookupEmptyable mem mem_aliases)
then
-- If not existential, update the potential last use of any existential
-- memory aliasing it, but do not set the potential last use of the
-- memory itself, since there are cycles in loops, and it must also
-- contain the same data in the next iteration, so it can never be
-- reused inside the loop body, and must therefore always have its last
-- use outside the body. But since the existential memory might in the
-- current iteration refer to it, its last use needs to be updated.
-- Note that while it is not wrong to run the code below also when the
-- memory has its first use inside the body, in that case it should not
-- be necessary, since we would be outside the body by then, and it
-- would result in a too conservative analysis. As an example, see
-- tests/mix/loop-interference-use.fut.
when from_outer $ do
-- If the memory has its first use outside the current body, we need
-- to find its actual last use (if it occurs in the body) through
-- memory aliases.
--
-- If memory block t aliases memory block u (meaning that the memory of
-- t *can* be the memory of u), and u has a potential last use here,
-- then t also has a potential last use here (the relation is not
-- commutative, so it does not work the other way round).
let reverse_mem_aliases = M.keys $ M.filter (mem `S.member`) mem_aliases
exclude = S.singleton mem
forM_ reverse_mem_aliases $ \mem' ->
setOptimistic mem' (FromStm x) exclude
else
-- Just set the last use.
unless from_outer $ setOptimistic mem (FromStm x) S.empty
withLocalCurFirstUses $ mMod $ fullWalkExpM walker walker_kernel e
where walker = identityWalker
{ walkOnBody = lookInBody }
walker_kernel = identityKernelWalker
{ walkOnKernelBody = coerce . lookInBody
, walkOnKernelKernelBody = coerce . lookInKernelBody
, walkOnKernelLambda = coerce . lookInBody . lambdaBody
}
-- Look in body results.
lookInRes :: SubExp -> FindM lore ()
lookInRes (Var v) = do
exis <- asks ctxExistentials
-- If v is a existential variable, there is no reason to record its last use,
-- as existential memory cannot be reused (this is also the case for other
-- setOptimistic calls, but not in a clear way).
unless (v `S.member` exis) $ do
mem_v <- M.lookup v <$> asks ctxVarToMem
case mem_v of
Just mem ->
setOptimistic (memSrcName mem) (FromRes v) S.empty
Nothing ->
return ()
lookInRes _ = return ()
-- Some freeInExp results are too limiting and give us too conservative last use
-- results (especially in the CPU pipeline). We only care about a free variable
-- if we *read* from it. If it only exists for *writing*, then we don't have to
-- look at its memory, since whatever is there we overwrite, and so there cannot
-- be any last *use*.
freeExcludes :: Exp lore -> [VName]
freeExcludes e = case e of
DoLoop _ _mergevalparams _ _ ->
-- FIXME: If the returned memory block-associated mergevalparams do not come
-- directly from a Scratch creation, we should be able to ignore them and
-- thereby become less conservative.
[]
BasicOp (Update orig _ _) ->
[orig]
_ -> []