diff options
Diffstat (limited to 'source/slang/ir-dce.cpp')
| -rw-r--r-- | source/slang/ir-dce.cpp | 325 |
1 files changed, 325 insertions, 0 deletions
diff --git a/source/slang/ir-dce.cpp b/source/slang/ir-dce.cpp new file mode 100644 index 000000000..0f037bfe5 --- /dev/null +++ b/source/slang/ir-dce.cpp @@ -0,0 +1,325 @@ +// ir-dce.cpp +#include "ir-dce.h" + +#include "ir.h" +#include "ir-insts.h" + +namespace Slang +{ + +struct DeadCodeEliminationContext +{ + // This type implements a simple global DCE pass over + // an entire module. + // + // We start with member variables to stand in for + // the parameters that were passed to the top-level + // `eliminateDeadCode` function. + // + CompileRequest* compileRequest; + IRModule* module; + + // Our overall process is going to be to determine + // which instructions in the module are "live" + // and then eliminate anything that wasn't found to + // be live. + // + // We will track the liveness state by keeping + // a set of all instructions we have so far determined + // to be live. + // + HashSet<IRInst*> liveInsts; + + // Querying whether an instruction has been + // determined to be live is easy. + // + bool isInstLive(IRInst* inst) + { + // The only wrinkle is that we want to safeguard + // against a null instruction (there are some + // corner cases where we still construct IR + // instructions with a null type). + // + if(!inst) return false; + + return liveInsts.Contains(inst); + } + + // We are going to do an iterative analysis + // where we mark instructions we know are + // live, and then see if that can help us + // identify any other instructions that + // must also be live. + // + // For this, we will use a work list of + // instructions that have been marked + // as live, but for which we haven't + // looked at their impact on other + // instructions. + // + List<IRInst*> workList; + + // When we discover that an instruction seems + // to be live, we will add it to our set, + // and also the work list, but only if we + // haven't done so previously. + // + void markInstAsLive(IRInst* inst) + { + // Again, we safeguard against null instructions + // just in case. + // + if(!inst) return; + + if(liveInsts.Contains(inst)) + return; + liveInsts.Add(inst); + workList.Add(inst); + } + + // Given the basic infrastructrure above, let's + // dive into the task of actually finding all + // the live code in a module. + // + void processModule() + { + // First of all, we know that the root module instruction + // should be considered as live, because otherwise + // we'd end up eliminating it, so that is a + // good place to start. + // + markInstAsLive(module->getModuleInst()); + + // Marking the module as live should have + // seeded our work list, so we can now start + // processing entries off of our work list + // until it goes dry. + // + while( workList.Count() ) + { + auto inst = workList.Last(); + workList.RemoveLast(); + + // At this point we know that `inst` is live, + // and we want to start considering which other + // instructions must be live because of that + // knowlege. + // + // A first easy case is that the parent (if any) + // of a live instruction had better be live, or + // else we might delete the parent, and + // the child with it. + // + markInstAsLive(inst->getParent()); + + // Next the type of a live instruction, and all + // of its operands must also be live, or else + // we won't be able to compute its value. + // + markInstAsLive(inst->getFullType()); + UInt operandCount = inst->getOperandCount(); + for( UInt ii = 0; ii < operandCount; ++ii ) + { + markInstAsLive(inst->getOperand(ii)); + } + + // Finally, we need to consider the children + // and decorations of the instruction. + // + // Note that just because an instruction is + // live doesn't mean its children must be, or + // else we'd never eliminate *anything* (we + // marked the whole module as live, and everything + // is a transitive child of the module). + // + // Decorations, in contrast, are always live if their + // parents are (because we don't want to silently drop + // decorations). It is still important to *mark* + // decorations as live, because they have operands, + // and those operands need to be marked as live. + // We will fold decorations into the same loop + // as children for simplicity. + // + // To keep the code here simple, we'll defer the + // decision of whether a child (or decoration) + // should be live when its parent is to a subroutine. + // + for( auto child : inst->getDecorationsAndChildren() ) + { + if(shouldInstBeLiveIfParentIsLive(child)) + { + // In this case, we know `inst` is live and + // its `child` should be live if its parent is, + // so the `child` must be live too. + // + markInstAsLive(child); + } + } + } + + // If our work list runs dry, that means we've reached a steady + // state where everything that is transitively relevant to + // the "outputs" of the module has been marked as live. + // + // Now we can simply walk through all of our instructions + // recursively and eliminate those that are "dead" by + // virtue of not having been found live. + // + eliminateDeadInstsRec(module->getModuleInst()); + } + + void eliminateDeadInstsRec(IRInst* inst) + { + // Given the instruction `inst` we need to eliminate + // any dead code at, or under it. + // + // The easy case is if `inst` is dead (that is, not live). + // + if( !isInstLive(inst) ) + { + // We can simply remove and deallocate `inst` because it is + // dead, and not worry about any of its descendents, + // because they must have been dead too (since we always + // mark the parent of a live instruction as live). + // + inst->removeAndDeallocate(); + } + else + { + // If `inst` is live, then we need to deal with the possibility + // that its children/decorations (or descendents in general) + // might still be dead. + // + // The biggest wrinkle is that we walk the linked list of + // children/decorations a bit carefully, using a temporary + // to hold the next node, in case we eliminate one of + // the children as we go. + // + IRInst* next = nullptr; + for( IRInst* child = inst->getFirstDecorationOrChild(); child; child = next ) + { + next = child->getNextInst(); + eliminateDeadInstsRec(child); + } + } + } + + // Now we come to the decision procedure we put off before: + // should a given `inst` be live if its parent is? + // + bool shouldInstBeLiveIfParentIsLive(IRInst* inst) + { + // The main source of confusion/complexity here is that + // we are using the same routine to decide: + // + // * Should some ordinary instruction in a basic block be kept around? + // * Should a basic block in some function be kept around? + // * Should a function/type/variable in a module be kept around? + // + // Still, there are a few basic patterns we can observe. + // First, if `inst` is an instruction that might have some effects + // when it is executed, then we should keep it around. + // + if(inst->mightHaveSideEffects()) + return true; + // + // The `mightHaveSideEffects` query is conservative, and will + // return `true` as its default mode, so once we are past that + // query we know that `inst` is either something "structural" + // (that makes up the program) rather than executable, or it + // is executable but was on a white list of things that are + // safe to eliminate. + + // Most top-level objects (functions, types, etc.) obviously + // do *not* have side effects. That creates the risk that + // we'll just go ahead and eliminate every single function/type + // in a module. There needs to be a way to identify the + // functions we want to keep around, and for right now + // that is handled with the `[entryPoint]` decoration. + // + if(inst->findDecorationImpl(kIROp_EntryPointDecoration)) + return true; + // + // TODO: Eventually it would make sense to consider everything + // with an `[export(...)]` decoration as live, but our current + // approach to linking for back-end compilation leaves many + // linkage decorations in place that we seemingly don't need/want. + + // A basic block is an interesting case. Knowing that a function + // is live means that its entry block is live, but the liveness + // of any other blocks is determined by whether they are referenced + // by other instructions (e.g., a branch from one block to + // another). + // + if( auto block = as<IRBlock>(inst) ) + { + // To determine whether this is the first block in its + // parent function (or what-have-you) we can simply + // check if there is a previous block before it. + // + auto prevBlock = block->getPrevBlock(); + return prevBlock == nullptr; + } + + // There are a few special cases of "structural" instructions + // that we don't want to eliminate, so we'll check for those next. + // + switch( inst->op ) + { + // Function parameters obviously shouldn't get eliminated, + // even if nothing references them, and block parameters + // (phi nodes) will be considered live when their block is, + // just so that we don't have to deal with any complications + // around re-writing the relevant inter-block argument passing. + // + // TODO: A smarter DCE pass could deal with this case more + // carefully, or we could improve the interprocedural SCCP + // pass to deal with block parameters instead. + // + case kIROp_Param: + return true; + + // IR struct types and witness tables are currently kludged + // so that they have child instructions that represent their + // entries (effectively `(key,value)` pairs), and those child + // instructions are never directly referenced (e.g., an access + // to a struct field references the *key* but not the `(key,value)` + // pair that is the `IRField` instruction. + // + // TODO: at some point the IR should use a different representation + // for struct types and witness tables that does away with + // this problem. + // + case kIROp_StructField: + case kIROp_WitnessTableEntry: + return true; + + default: + break; + } + + // If none of the explicit cases above matched, then we will consider + // the instruction to not be live just because its parent is. Further + // analysis could still lead to a change in the status of `inst`, if + // an instruction that uses it as an operand is marked live. + // + return false; + } +}; + +// The top-level function for invoking the DCE pass +// is straighforward. We set up the context object +// and then defer to it for the real work. +// +void eliminateDeadCode( + CompileRequest* compileRequest, + IRModule* module) +{ + DeadCodeEliminationContext context; + context.compileRequest = compileRequest; + context.module = module; + + context.processModule(); +} + +} |
