summaryrefslogtreecommitdiff
path: root/source/slang/slang-ir-liveness.cpp
diff options
context:
space:
mode:
authorjsmall-nvidia <jsmall@nvidia.com>2022-05-05 09:09:25 -0400
committerGitHub <noreply@github.com>2022-05-05 09:09:25 -0400
commite3e0132743ada1569cefe18bfbf54178330204c4 (patch)
treea85b3992f97f67a5520a520dd60677d382ee4ce6 /source/slang/slang-ir-liveness.cpp
parentef314f1b417e92b2fd27e3ed7f504a711c49231b (diff)
Preliminary Liveness tracking (#2218)
* #include an absolute path didn't work - because paths were taken to always be relative. * WIP tracking liveness. * Skeleton around adding liveness instructions. * Calling into liveness tracking logic. Adds live start to var insts. * Liveness macros have initial output. * Looking at different initialization scenarios. * Some discussion around liveness. * WIP for working out liveness end. * WIP Updated liveness using use lists. * Is now adding liveness information * Some small fixes. * WIP around liveness. * Seems to output liveness correctly for current scenario. * Tidy up liveness code. * Update comment arounds liveness to current status. * Small fixes to liveness test. * Add support for call in liveness analysis. * Improve liveness example with array access. * Small updates to comments. * Disable liveness test because inconsistencies with output on CI system. * Fix some issues brought up in PR. * Rename liveness instructions.
Diffstat (limited to 'source/slang/slang-ir-liveness.cpp')
-rw-r--r--source/slang/slang-ir-liveness.cpp533
1 files changed, 533 insertions, 0 deletions
diff --git a/source/slang/slang-ir-liveness.cpp b/source/slang/slang-ir-liveness.cpp
new file mode 100644
index 000000000..3d1626691
--- /dev/null
+++ b/source/slang/slang-ir-liveness.cpp
@@ -0,0 +1,533 @@
+#include "slang-ir-liveness.h"
+
+#include "slang-ir-insts.h"
+#include "slang-ir.h"
+
+#include "slang-ir-dominators.h"
+
+namespace Slang
+{
+
+
+/*
+Discussion
+==========
+
+Currently we are only tracking 'var'/IRVar local variables. They are accessed via pointers. This
+means
+
+* We don't need to care about extractField / extractElement, as they only work directly on the value
+* We need to track aliases created via getFieldPtr / getElementPtr
+* There is a distinction between a 'pointer' and an 'address'.
+ * A "pointer" can 'escape' just as in other languages, and is the general case
+ * If we are talking about an "address", then this is constrained by our language rules,
+
+NOTE! Confusingly there is getElementPtr and getFieldAddress (and getAddress). I also don't see Addr/Addr type as a distinct thing
+from Ptr, so I assume that differentiation is aspirational?
+
+A) We don't need to worry about a phi node temporary holding a pointer (or scope ending *on* the branch), because
+the phi node will pass the result by value, leading to a *load* before the branch..
+
+Other
+
+```
+ let foo : Ptr<SomeStruct> = var;
+...
+store(someOtherPtr, foo); // this is `store`, but not a store *to* foo!!!!!
+...
+```
+
+Here a *pointer* is being stored into someOtherPtr. This means all bets are off. Liveness will have to be assumed anywhere the
+variable is accessible.
+TODO(JS): Note that currently this scenario isn't handled by this algorithm.
+
+```
+ let foo : Ptr<SomeStruct> = var;
+ ...
+ br SomeOtherThing(foo); // OH NO!!!
+```
+
+It is believed this can't happen in current code. Leading to assertion A) above.
+
+* Long-term IR type-system thing: we should probably have an explicit instruction
+ that casts a local `Ptr<Foo>` to either an `Out<Foo>` or `InOut<Foo>` for exactly
+ these cases (and then use the *cast* operation to tell us what is going on).
+*/
+
+/*
+Take the code sequence
+
+```HLSL
+ SomeStruct s;
+ SomeStruct t = makeSomeStruct();
+ SomeStruct u = {};
+```
+
+Produces something like...
+
+```HLSL
+ SomeStruct_0 s_1;
+ SLANG_LIVE_START(s_1)
+ SomeStruct_0 t_0;
+ SLANG_LIVE_START(t_0)
+ SomeStruct_0 _S4 = makeSomeStruct_0();
+ t_0 = _S4;
+ SomeStruct_0 u_0;
+ SLANG_LIVE_START(u_0)
+ SomeStruct_0 _S6 = { ... };
+ u_0 = _S6;
+```
+
+This is good, in so far as the variables do get LIVE_START, however they are defined. It is perhaps 'bad' in so far as a temporary
+is created that is then just copied into the variable. That temporary being something that is mutable, and can be partially modified (it's a struct)
+could perhaps have liveness issues.
+*/
+
+namespace { // anonymous
+
+struct LivenessContext
+{
+ // NOTE! Care must be taken changing the order. Some checks rely on Found having a smaller value than `NotFound`.
+ // Allowing NotFound to be promoted to Found.
+ enum class FoundResult
+ {
+ Found, ///< All paths were either not dominated, found
+ NotFound, ///< It is dominated but no access was found
+ NotDominated, ///< If it's not dominated it can't have a liveness end
+ };
+
+ enum class AccessType
+ {
+ None, ///< There is no access
+ Alias, ///< Produces an alias to the root
+ Access, ///< Is an access to the root (perhaps through an alias)
+ };
+
+ struct RootInfo
+ {
+ IRInst* root;
+ IRLiveRangeStart* liveStart;
+ };
+
+ /// Processor a successor to a block
+ FoundResult processSuccessor(IRBlock* block);
+
+ /// Process a block
+ FoundResult processBlock(IRBlock* block);
+
+ /// Process a 'root'. A variable that has liveness tracking
+ void processRoot(const RootInfo& rootInfo);
+
+ /// Process a function in the module
+ void processFunction(IRFunc* funcInst);
+
+ /// Process the module
+ void processModule();
+
+ LivenessContext(IRModule* module):
+ m_module(module)
+ {
+ m_sharedBuilder.init(module);
+ m_builder.init(m_sharedBuilder);
+ }
+
+ // Add a live end instruction at the start of block, referencing the root 'root'.
+ void _addLiveRangeEndAtBlockStart(IRBlock* block, IRInst* root);
+
+ /// Find the last instruction in block that accesses one of the roots or it's aliases
+ /// Requires that m_accessSet contains all of the accesses
+ /// Returns nullptr if no access instruction was found in the block.
+ IRInst* _findLastAccessInBlock(IRBlock* block);
+
+ /// Add a result for the block
+ /// Allows for promotion from NotFound -> Found if there is already a result
+ FoundResult _addResult(IRBlock* block, FoundResult result);
+
+ RefPtr<IRDominatorTree> m_dominatorTree; ///< The dominator tree for the current function
+
+ IRInst* m_root; ///< The root item we are searching for accesses to, to determine scope/liveness
+ IRBlock* m_rootBlock; ///< The block the root is in
+
+ HashSet<IRInst*> m_accessSet; ///< Holds a set of all the functions that in some way access the root.
+
+ List<IRInst*> m_aliases; ///< A list of instructions that alias to the root
+
+ Dictionary<IRBlock*, FoundResult> m_blockResult; ///< Cached result for each block
+
+ IRModule* m_module;
+ SharedIRBuilder m_sharedBuilder;
+ IRBuilder m_builder;
+};
+
+void LivenessContext::_addLiveRangeEndAtBlockStart(IRBlock* block, IRInst* root)
+{
+ // Insert before the first ordinary inst
+ auto inst = block->getFirstOrdinaryInst();
+ SLANG_ASSERT(inst);
+
+ m_builder.setInsertLoc(IRInsertLoc::before(inst));
+
+ // Add the live end inst
+ m_builder.emitLiveRangeEnd(root);
+}
+
+IRInst* LivenessContext::_findLastAccessInBlock(IRBlock* block)
+{
+ // Search the instructions in this block in reverse order, to find first access
+ for (IRInst* cur = block->getLastChild(); cur; cur = cur->getPrevInst())
+ {
+ if (m_accessSet.Contains(cur))
+ {
+ return cur;
+ }
+ }
+ return nullptr;
+}
+
+LivenessContext::FoundResult LivenessContext::_addResult(IRBlock* block, FoundResult result)
+{
+ auto currentResultPtr = m_blockResult.TryGetValueOrAdd(block, result);
+ if (currentResultPtr)
+ {
+ const auto currentResult = *currentResultPtr;
+ // If it were NotDominated, it cannot be promoted to Found/NotFound.
+ SLANG_ASSERT(currentResult != FoundResult::NotDominated);
+
+ // We can only promote from NotFound -> Found
+
+ SLANG_ASSERT(Index(result) <= Index(currentResult));
+
+ // Set the new result
+ *currentResultPtr = result;
+ }
+
+ return result;
+}
+
+LivenessContext::FoundResult LivenessContext::processSuccessor(IRBlock* block)
+{
+ // Check if there is already a result for this block.
+ // If there is just return that.
+ auto res = m_blockResult.TryGetValue(block);
+ if (res)
+ {
+ return *res;
+ }
+
+ // If the block is *not* dominated by the root block, we know it can't
+ // end liveness.
+ // Return that it is not dominated, and add to the cache for the block
+ if (!m_dominatorTree->properlyDominates(m_rootBlock, block))
+ {
+ return _addResult(block, FoundResult::NotDominated);
+ }
+
+ // Else process the block to try and find the last used instruction
+ return processBlock(block);
+}
+
+LivenessContext::FoundResult LivenessContext::processBlock(IRBlock* block)
+{
+ // Find all the successors for this block
+ auto successors = block->getSuccessors();
+ const Index count = successors.getCount();
+
+ // We need to store of the results for successors
+ List<FoundResult> successorResults;
+ successorResults.setCount(count);
+
+ Index foundCount = 0;
+ Index notDominatedCount = 0;
+
+ {
+ auto cur = successors.begin();
+ for (Index i = 0; i < count; ++i, ++cur)
+ {
+ auto succ = *cur;
+
+ // Process the successor
+ const auto successorResult = processSuccessor(succ);
+
+ // Store the result
+ successorResults[i] = successorResult;
+
+ // Change counts depending on the result
+ foundCount += Index(successorResult == FoundResult::Found);
+ notDominatedCount += Index(successorResult == FoundResult::NotDominated);
+ }
+ }
+
+ // If one or more of the successors (or successors of successors),
+ // was found to have the last access, we need to mark the end of scope
+ // at the start of any other paths (which are dominated).
+ if (foundCount > 0)
+ {
+ // If all successors have result, or are not dominated
+ if (foundCount + notDominatedCount == count)
+ {
+ return _addResult(block, FoundResult::Found);
+ }
+
+ // We want to place an end scope in all blocks where it wasn't found
+ auto cur = successors.begin();
+ for (Index i = 0; i < count; ++i, ++cur)
+ {
+ auto successor = *cur;
+ const auto successorResult = successorResults[i];
+ if (successorResult == FoundResult::NotFound)
+ {
+ _addLiveRangeEndAtBlockStart(successor, m_root);
+ _addResult(successor, FoundResult::Found);
+ }
+ }
+
+ // This block, can be marked as found as all successors are either not dominated, found
+ // or have have
+ return _addResult(block, FoundResult::Found);
+ }
+
+ // Search the instructions in this block in reverse order, to find first access
+ IRInst* lastAccess = _findLastAccessInBlock(block);
+
+ // Wasn't an access so we are done
+ if (lastAccess == nullptr)
+ {
+ return _addResult(block, FoundResult::NotFound);
+ }
+
+ // Can never be a terminator, because logic to find the access instructions does not
+ // include terminators.
+ SLANG_ASSERT(as<IRTerminatorInst>(lastAccess) == nullptr);
+
+ // Just add end of scope after the inst
+ m_builder.setInsertLoc(IRInsertLoc::after(lastAccess));
+
+ // Add the live end inst
+ m_builder.emitLiveRangeEnd(m_root);
+ return _addResult(block, FoundResult::Found);
+}
+
+void LivenessContext::processRoot(const RootInfo& rootInfo)
+{
+ // Clear the work structures
+ m_accessSet.Clear();
+ m_aliases.clear();
+ m_blockResult.Clear();
+
+ auto root = rootInfo.root;
+
+ // Store root information, so don't have to pass around methods
+ m_rootBlock = as<IRBlock>(root->parent);
+ m_root = root;
+
+ // Add the root to the list of aliases, to start lookup
+ m_aliases.add(root);
+
+ // The challenge here is to try and determine when a root is no longer accessed, and so is no longer live
+ //
+ // Note that a root can be accessed directly, but also through `aliases`. For example if the root is a structure
+ // a pointer to a field in the root would be an alias.
+ //
+ // In terms of liveness, the only accesses that are important are loads. This is because if the last operation on
+ // a root/alias is a store, if it is never read it will never be seen, so in effect doesn't matter.
+ //
+ // The algorithm here works as follows
+ // 0) Prior to this function, a dominator tree is built for the function
+ // This is usefuly because variables defined in block A, is only accessible to blocks *dominated* by A
+ // 1) Deterime all of the aliases, and accesses to the root
+ // Add all the access instructions into m_accessSet
+ // Add all the aliases to m_aliases
+
+ for (Index i = 0; i < m_aliases.getCount(); ++i)
+ {
+ IRInst* alias = m_aliases[i];
+
+ // Find all the uses of this alias/root
+ for (IRUse* use = alias->firstUse; use; use = use->nextUse)
+ {
+ IRInst* cur = use->getUser();
+ IRInst* base = nullptr;
+
+ IRBlock* block = as<IRBlock>(cur->getParent());
+ if (!block)
+ {
+ continue;
+ }
+
+ AccessType accessType = AccessType::None;
+
+ // We want to find instructions that access the root
+ switch (cur->getOp())
+ {
+
+ case kIROp_getElementPtr:
+ {
+ base = static_cast<IRGetElementPtr*>(cur)->getBase();
+ accessType = AccessType::Alias;
+ break;
+ }
+ case kIROp_FieldAddress:
+ {
+ base = static_cast<IRFieldAddress*>(cur)->getBase();
+ accessType = AccessType::Alias;
+ break;
+ }
+ case kIROp_getAddr:
+ {
+ IRGetAddress* getAddr = static_cast<IRGetAddress*>(cur);
+ base = getAddr->getOperand(0);
+ accessType = AccessType::Alias;
+ break;
+ }
+ case kIROp_Call:
+ {
+ // TODO(JS): This is arguably too conservative.
+ //
+ // Depending on how the parameter is used - in, out, inout changes the interpretation
+ //
+ // *If we are talking about a real "pointer" then this is basically the general case again.
+ // the callee could store the pointer into a global, dictionary, whatever.
+ //
+ // * If we are talking about an "address", then this is constrained by our language rules,
+ // and we kind of need to find the type of the matching parameter :
+ // * If the parameter is an `out` parameter, this is basically like a `store`
+ // * If the parameter is an `inout` parameter, this is basically like a `load`
+
+ // We can assume it accesses the base
+ base = alias;
+ accessType = AccessType::Access;
+ break;
+ }
+ case kIROp_Load:
+ {
+ // We only care about loads in terms of identifying liveness
+ base = static_cast<IRLoad*>(cur)->getPtr();
+ accessType = AccessType::Access;
+ break;
+ }
+ case kIROp_Store:
+ {
+ // In terms of liveness, stores can be ignored
+ break;
+ }
+ case kIROp_getElement:
+ case kIROp_FieldExtract:
+ {
+ // These will never take place on the var which is accessed through a pointer, so can be ignored
+ break;
+ }
+ default: break;
+ }
+
+ // Make sure the access is through the alias (as opposed to some other part of the instructions 'use')
+ if (base == alias)
+ {
+ switch (accessType)
+ {
+ case AccessType::Alias:
+ {
+ // Add this instruction to the aliases
+ m_aliases.add(cur);
+ break;
+ }
+ case AccessType::Access:
+ {
+ m_accessSet.Add(cur);
+ break;
+ }
+ default: break;
+ }
+ }
+ }
+ }
+
+ // Now we want to find the last access in the graph of successors
+ //
+ // This works by recursively starting from the block where the variable is defined, walking depth first the graph of
+ // successors. We cache the results in m_blockResult
+ //
+ // There is an extra caveat around the dominator tree. If we just traversed the successors, if there is a loop
+ // we'd end up in an infinite loop. We can avoid this because we know that the root is only available in blocks dominated
+ // by the root.
+
+ {
+ auto foundResult = processBlock(m_rootBlock);
+
+ if (foundResult == FoundResult::NotFound)
+ {
+ // Means there is no access to this variable(!)
+ // Which means we can end the scope, after the the start scope
+ m_builder.setInsertLoc(IRInsertLoc::after(rootInfo.liveStart));
+ m_builder.emitLiveRangeEnd(root);
+ }
+ }
+}
+
+void LivenessContext::processFunction(IRFunc* funcInst)
+{
+ List<RootInfo> rootInfos;
+
+ // Iterate through blocks in the function, looking for variables to live track
+ for (auto block = funcInst->getFirstBlock(); block; block = block->getNextBlock())
+ {
+ for (auto inst = block->getFirstChild(); inst; inst = inst->getNextInst())
+ {
+ // We look for var declarations.
+ if (auto varInst = as<IRVar>(inst))
+ {
+ // Add the start location
+ m_builder.setInsertLoc(IRInsertLoc::after(varInst));
+
+ // Emit the start
+ auto liveStart = m_builder.emitLiveRangeStart(varInst);
+
+ // Add as a root
+ RootInfo rootInfo;
+ rootInfo.root = varInst;
+ rootInfo.liveStart = liveStart;
+
+ rootInfos.add(rootInfo);
+ }
+ }
+ }
+
+ // Create the dominator tree.
+ m_dominatorTree = computeDominatorTree(funcInst);
+
+ // Process the roots
+ for (const auto& rootInfo : rootInfos)
+ {
+ processRoot(rootInfo);
+ }
+}
+
+void LivenessContext::processModule()
+{
+ // When we process liveness, is prior to output for a target
+ // So post specialization
+
+ IRModuleInst* moduleInst = m_module->getModuleInst();
+
+ for (IRInst* child = moduleInst->getFirstDecorationOrChild(); child; child = child->getNextInst())
+ {
+ // We want to find all of the functions, and process them
+ if (auto funcInst = as<IRFunc>(child))
+ {
+ // Then we want to look through their definition
+ // inserting instructions that mark the liveness start/end
+ processFunction(funcInst);
+ }
+ }
+}
+
+} // anonymous
+
+void addLivenessTrackingToModule(IRModule* module)
+{
+ LivenessContext context(module);
+
+ context.processModule();
+}
+
+} // namespace Slang
+ \ No newline at end of file