https://github.com/shader-slang/slang
Raw File
Tip revision: 5902acdabc4445a65741a7a6a3a95f223e301059 authored by Yong He on 23 January 2024, 07:19:40 UTC
[LSP] Fetch configs directly from didConfigurationChanged message. (#3478)
Tip revision: 5902acd
slang-ir-dce.cpp
// slang-ir-dce.cpp
#include "slang-ir-dce.h"

#include "slang-ir.h"
#include "slang-ir-insts.h"
#include "slang-ir-util.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.
    //
    IRModule*                       module;

    IRDeadCodeEliminationOptions    options;

    // If we removed an inst, there may be still "weak references" to the inst.
    // These uses will be replaced with `undefInst`.
    IRInst* undefInst = nullptr;

    // Track if we have removed any phi parameters.
    // If so we need to rerun dce pass because after removing them
    // there could be new DCE opportunities.
    bool phiRemoved = false;

    // Querying whether an instruction has been
    // determined to be live is easy.
    // To speedup the test, we use the
    // `scratchData` field of each inst as the marker.
    //
    bool isInstAlive(IRInst* inst)
    {
        if (!inst) return false;
        return inst->scratchData != 0;
    }

    // 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 (!inst->scratchData)
        {
            inst->scratchData = 1;
            workList.add(inst);
        }
    }

    IRInst* getUndefInst()
    {
        if (!undefInst)
        {
            IRBuilder builder(module);
            if (auto firstChild = module->getModuleInst()->getFirstChild())
                builder.setInsertBefore(firstChild);
            else
                builder.setInsertInto(module->getModuleInst());
            undefInst = Slang::getUndefInst(builder, module);
        }
        return undefInst;
    }

    bool processInst(IRInst* root)
    {
        bool result = false;

        module->invalidateAllAnalysis();

        for (;;)
        {
            // Clear the `alive` bits by initializing all scratchData to 0.
            initializeScratchData(root);

            workList.clear();

            // First of all, we know that the root instruction
            // should be considered as live, because otherwise
            // we'd end up eliminating it, so that is a
            // good place to start.
            //
            markInstAsLive(root);

            // Ensure there is a global undef inst that is always alive.
            // This undef inst will be used to fill in weak-referencing uses
            // whose used value is marked as dead and eliminated.
            // We always make sure this undef inst is available to prevent
            // infiniate oscilating loops.
            markInstAsLive(getUndefInst());

            // 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.getCount())
            {
                auto inst = workList.getLast();
                workList.removeLast();

                if (!isChildInstOf(inst, root))
                    continue;

                // 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)
                {
                    // There are some type of operands that needs to be treated as
                    // "weak" references -- they can never hold things alive, and
                    // whenever we delete the referenced value, these operands needs
                    // to be replaced with `undef`.
                    if (!isWeakReferenceOperand(inst, 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.
            //
            phiRemoved = false;
            result |= eliminateDeadInstsRec(root);


            if (!phiRemoved)
                break;
        }
        return result;
    }

    // Given the basic infrastructrure above, let's
    // dive into the task of actually finding all
    // the live code in a module.
    //
    bool processModule()
    {
        return processInst(module->getModuleInst());
    }

    bool eliminateDeadInstsRec(IRInst* inst)
    {
        bool changed = false;
        // 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( !isInstAlive(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).
            //
            if (inst->hasUses())
            {
                inst->replaceUsesWith(getUndefInst());
            }

            if (inst->getOp() == kIROp_Param)
            {
                // For Phi parameters, we need to update all branch arguments.
                removePhiArgs(inst);
                phiRemoved = true;
            }
            inst->removeAndDeallocate();
            changed = true;
        }
        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, because eliminating one inst
            // may cause the other nodes to be hoisted out of the current scope.
            // We need to cache all children in a work list to ensure they are
            // properly traversed.
            //
            List<IRInst*> children;
            for (auto child : inst->getDecorationsAndChildren())
                children.add(child);
            for(IRInst* child : children)
            {
                changed |= eliminateDeadInstsRec(child);
            }
            if (changed)
            {
                // If the function body is changed, invalidate its dominator tree.
                if (auto func = as<IRGlobalValueWithCode>(inst))
                    module->invalidateAnalysisForInst(func);
            }
        }
        return changed;
    }

    // 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)
    {
        return Slang::shouldInstBeLiveIfParentIsLive(inst, options);
    }
};

bool isFirstBlock(IRInst* inst)
{
    auto block = as<IRBlock>(inst);
    if (!block)
        return false;
    if (!block->getParent())
        return false;
    return block->getParent()->getFirstBlock() == block;
}

bool shouldInstBeLiveIfParentIsLive(IRInst* inst, IRDeadCodeEliminationOptions options)
{
    // 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(SideEffectAnalysisOptions::UseDominanceTree))
    {
        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 an allow-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 `[keepAlive]` decoration.
    //
    if (inst->findDecorationImpl(kIROp_KeepAliveDecoration))
        return true;
    //
    // We also consider anything with an `[export(...)]` as live,
    // when the appropriate option has been set.
    //
    // Note: our current approach to linking for back-end compilation
    // leaves many linakge decorations in place that we seemingly
    // don't need/want, so this option currently can't be enabled
    // unconditionally.
    //
    if (options.keepExportsAlive)
    {
        bool isImported = false;
        bool shouldKeptAliveIfImported = false;
        IRInst* innerInst = inst;
        if (auto genInst = as<IRGeneric>(inst))
        {
            innerInst = findInnerMostGenericReturnVal(genInst);
        }
        for (auto decor : inst->getDecorations())
        {
            switch (decor->getOp())
            {
            case kIROp_ExportDecoration:
                return true;
            case kIROp_ImportDecoration:
                isImported = true;
                break;
            }
        }
        for (auto decor : innerInst->getDecorations())
        {
            switch (decor->getOp())
            {
            case kIROp_ForwardDerivativeDecoration:
            case kIROp_UserDefinedBackwardDerivativeDecoration:
            case kIROp_PrimalSubstituteDecoration:
                shouldKeptAliveIfImported = true;
                break;
            }
        }
        if (isImported && shouldKeptAliveIfImported)
            return true;
    }

    if (options.keepLayoutsAlive && inst->findDecoration<IRLayoutDecoration>())
    {
        return true;
    }

    // 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->getOp())
    {
        // Function parameters obviously shouldn't get eliminated,
        // even if nothing references them.
        //
    case kIROp_Param:
        return isFirstBlock(inst->getParent());

        // 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;
}

bool isWeakReferenceOperand(IRInst* inst, UInt operandIndex)
{
    // There are some type of operands that needs to be treated as
    // "weak" references -- they can never hold things alive, and
    // whenever we delete the referenced value, these operands needs
    // to be replaced with `undef`.
    switch (inst->getOp())
    {
    case kIROp_BoundInterfaceType:
        if (inst->getOperand(operandIndex)->getOp() == kIROp_WitnessTable)
            return true;
        break;
    case kIROp_SpecializationDictionaryItem:
        // Ignore all operands of SpecializationDictionaryItem.
        // This inst is used as a cache and shouldn't hold anything alive.
        return true;
    default:
        break;
    }
    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.
//
bool eliminateDeadCode(
    IRModule*                           module,
    IRDeadCodeEliminationOptions const& options)
{
    DeadCodeEliminationContext context;
    context.module = module;
    context.options = options;
    return context.processModule();
}

bool eliminateDeadCode(
    IRInst* root,
    IRDeadCodeEliminationOptions const& options)
{
    DeadCodeEliminationContext context;
    context.module = root->getModule();
    context.options = options;
    return context.processInst(root);
}

}
back to top