diff options
| author | Yong He <yonghe@outlook.com> | 2023-07-19 13:50:49 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-07-19 13:50:49 -0700 |
| commit | 1cfb1c85b52e00cde2d21874a88cda2c22d18b62 (patch) | |
| tree | a38b24534d865ffe33a3d0fc030f5449ba729e28 /source/slang/slang-ir-specialize.cpp | |
| parent | 1fe5e83f3dcc8ef0efa2dd083ebdfab5d0f101a9 (diff) | |
Optimize specialization, and remove unnecessary calls to `simplifyIR`. (#2999)
* Remove unneccessary calls to `simplifyIR`.
* fix.
* Delete obsolete hoistConst pass.
* Fix.
* Small improvements.
* Fix.
* Fix enum lowering.
* fix
* tweaks.
* tweaks.
---------
Co-authored-by: Yong He <yhe@nvidia.com>
Diffstat (limited to 'source/slang/slang-ir-specialize.cpp')
| -rw-r--r-- | source/slang/slang-ir-specialize.cpp | 289 |
1 files changed, 79 insertions, 210 deletions
diff --git a/source/slang/slang-ir-specialize.cpp b/source/slang/slang-ir-specialize.cpp index a806a13e6..f1de6e408 100644 --- a/source/slang/slang-ir-specialize.cpp +++ b/source/slang/slang-ir-specialize.cpp @@ -50,20 +50,10 @@ struct SpecializationContext bool changed = false; - // We know that we can only perform generic specialization when all - // of the arguments to a generic are also fully specialized. - // The "is fully specialized" condition is something we - // need to solve for over the program, because the fully- - // specialized-ness of an instruction depends on the - // fully-specialized-ness of its operands. - // - // We will build an explicit hash set to encode those - // instructions that are fully specialized. - // - HashSet<IRInst*>& fullySpecializedInsts; SpecializationContext(IRModule* inModule) - : fullySpecializedInsts(*module->getContainerPool().getHashSet<IRInst>()) + : workList(*inModule->getContainerPool().getList<IRInst>()) + , workListSet(*inModule->getContainerPool().getHashSet<IRInst>()) , cleanInsts(*module->getContainerPool().getHashSet<IRInst>()) , module(inModule) { @@ -83,42 +73,54 @@ struct SpecializationContext // if (!inst) return true; - // An interface requirement entry should always be considered - // to be fully specialized, even if it hasn't been visited. - // - // Note: This logic is here to stop a circularity, where we - // can't mark an interface as used until its requirements are - // used, etc. - // - if (inst->getOp() == kIROp_InterfaceRequirementEntry) - return true; - - // A generic parameter is never specialized. switch (inst->getOp()) { case kIROp_GlobalGenericParam: + case kIROp_LookupWitness: return false; - case kIROp_Param: - if (inst->getParent() && inst->getParent()->getOp() == kIROp_Block && - inst->getParent()->getParent() && - inst->getParent()->getParent()->getOp() == kIROp_Generic) + case kIROp_Specialize: + // The `specialize` instruction is a bit sepcial, + // because it is possible to have a `specialize` + // of a built-in type so that it never gets + // substituted for another type. (e.g., the specific + // case where this code path first showed up + // as necessary was `RayQuery<>`) + // + { + auto specialize = cast<IRSpecialize>(inst); + auto base = specialize->getBase(); + if (auto generic = as<IRGeneric>(base)) + { + // If the thing being specialized can be resolved, + // *and* it is a target intrinsic, ... + // + if (auto result = findGenericReturnVal(generic)) + { + if (result->findDecoration<IRTargetIntrinsicDecoration>()) + { + // ... then we should consider the instruction as + // "fully specialized" in the same cases as for + // any ordinary instruciton. + // + + if (areAllOperandsFullySpecialized(inst)) + { + return true; + } + } + } + } return false; + } } - // A global value is always specialized. + // The default case is that a global value is always specialized. if (inst->getParent() == module->getModuleInst()) { - switch (inst->getOp()) - { - case kIROp_LookupWitness: - case kIROp_Specialize: - return false; - default: - return true; - } + return true; } - return fullySpecializedInsts.contains(inst); + return false; } // When an instruction isn't fully specialized, but its operands *are* @@ -146,34 +148,17 @@ struct SpecializationContext // to be considered for specialization or simplification, // whether generic, existential, etc. // - OrderedHashSet<IRInst*> workList; + List<IRInst*>& workList; + HashSet<IRInst*>& workListSet; HashSet<IRInst*>& cleanInsts; void addToWorkList( IRInst* inst) { -#if 0 - // Note(Yong): we should no longer ignore generic functions - // because they maybe called via dynamic dispatch. - // We still want to specialize calls inside a generic function - // if we can derive its type at compile time. The following - // skipping logic is disabled and we should consider remove it. - // - // - // We will ignore any code that is nested under a generic, - // because it doesn't make sense to perform specialization - // on such code. - // - for (auto ii = inst->getParent(); ii; ii = ii->getParent()) + if (workListSet.add(inst)) { - if (as<IRGeneric>(ii)) - return; - } -#endif + workList.add(inst); - if (workList.add(inst)) - { - cleanInsts.remove(inst); addUsersToWorkList(inst); } @@ -195,24 +180,6 @@ struct SpecializationContext } } - // One of the main transformations we will apply is to - // consider an instruction as being fully specialized. - // - void markInstAsFullySpecialized( - IRInst* inst) - { - if (fullySpecializedInsts.contains(inst)) - return; - fullySpecializedInsts.add(inst); - - // If we know that an instruction is fully specialized, - // then we should start to consider its uses and children - // as candidates for being fully specialized too... - // - addUsersToWorkList(inst); - } - - // Of course, somewhere along the way we expect // to run into uses of `specialize(...)` instructions // to bind a generic to arguments that we want to @@ -499,99 +466,6 @@ struct SpecializationContext return true; } - // Generic specialization depends on identifying when - // instructions are fully specialized. - // - void maybeMarkAsFullySpecialized( - IRInst* inst) - { - // TODO: The logic here is completely bogus and - // we need to revisit the notion of fully-specialized-ness - // to only involve things that are semantically *values* - // rather than computations/expressions. - // - // The rules should be something like: - // - // * Literals are values - // * Composite type constructors where all the operands are value are values - // * References to nominal types are values - // * Built-in types where all the operands are values are values - // - // The system for defining value-ness probably needs - // to combine with the system for deduplicating instructions, - // since values are an important class of instruction we want - // to deduplicate. - - switch (inst->getOp()) - { - default: - // The default case is that an instruction can - // be considered as fully specialized as soon - // as all of its operands are. - // - // Anything defined in global scope can be viewed as fully specialized. - if (inst->getParent() == module->getModuleInst() || - areAllOperandsFullySpecialized(inst)) - { - markInstAsFullySpecialized(inst); - } - break; - - // Certain instructions cannot ever be considered - // fully specialized because they should never - // be substituted into a generic as its arguments. - case kIROp_LookupWitness: - case kIROp_ExtractExistentialType: - case kIROp_BindExistentialsType: - break; - - // An interface type is always fully specialized. - case kIROp_InterfaceType: - markInstAsFullySpecialized(inst); - break; - - case kIROp_Specialize: - // The `specialize` instruction is a bit sepcial, - // because it is possible to have a `specialize` - // of a built-in type so that it never gets - // substituted for another type. (e.g., the specific - // case where this code path first showed up - // as necessary was `RayQuery<>`) - // - { - auto specialize = cast<IRSpecialize>(inst); - auto base = specialize->getBase(); - if (auto generic = as<IRGeneric>(base)) - { - // If the thing being specialized can be resolved, - // *and* it is a target intrinsic, ... - // - if (auto result = findGenericReturnVal(generic)) - { - if (result->findDecoration<IRTargetIntrinsicDecoration>()) - { - // ... then we should consider the instruction as - // "fully specialized" in the same cases as for - // any ordinary instruciton. - // - - if (areAllOperandsFullySpecialized(inst)) - { - markInstAsFullySpecialized(inst); - } - return; - } - } - } - - // Otherwise, a `specialize` instruction falls into - // the case of instructions that should never be - // considered to be fully specialized. - } - break; - } - } - // The core of this pass is to look at one instruction // at a time, and try to perform whatever specialization // is appropriate based on its opcode. @@ -739,6 +613,10 @@ struct SpecializationContext template<typename TDict> void _readSpecializationDictionaryImpl(TDict& dict, IRInst* dictInst) { + int childrenCount = 0; + for (auto child = dictInst->getFirstChild(); child; child = child->next) + childrenCount++; + dict.reserve(1 << Math::Log2Ceil(childrenCount * 2)); for (auto child : dictInst->getChildren()) { auto item = as<IRSpecializationDictionaryItem>(child); @@ -881,10 +759,11 @@ struct SpecializationContext for (;;) { bool iterChanged = false; - addToWorkList(module->getModuleInst()); - - while (workList.getCount() != 0) + for (;;) { + bool hasSpecialization = false; + addToWorkList(module->getModuleInst()); + // We will then iterate until our work list goes dry. // while (workList.getCount() != 0) @@ -892,23 +771,17 @@ struct SpecializationContext IRInst* inst = workList.getLast(); workList.removeLast(); - - cleanInsts.add(inst); + workListSet.remove(inst); // For each instruction we process, we want to perform // a few steps. // - // First we will do any checking required to tag an - // instruction as being fully specialized. - // - maybeMarkAsFullySpecialized(inst); - - // Next we will look for all the general-purpose + // First we will look for all the general-purpose // specialization opportunities (generic specialization, // existential specialization, simplifications, etc.) // if (inst->hasUses() || inst->mightHaveSideEffects()) - iterChanged |= maybeSpecializeInst(inst); + hasSpecialization |= maybeSpecializeInst(inst); // Finally, we need to make our logic recurse through // the whole IR module, so we want to add the children @@ -932,8 +805,10 @@ struct SpecializationContext addToWorkList(child); } } - - addDirtyInstsToWorkListRec(module->getModuleInst()); + if (hasSpecialization) + iterChanged = true; + else + break; } if (iterChanged) @@ -941,22 +816,22 @@ struct SpecializationContext this->changed = true; eliminateDeadCode(module->getModuleInst(), IRDeadCodeEliminationOptions()); } - else - { - // If we run out of specialization opportunities, consider - // lower lookupWitnessMethod insts into dynamic dispatch calls. - iterChanged = lowerWitnessLookup(module, sink); - if (!iterChanged || sink->getErrorCount()) - break; - } + + // Once the work list has gone dry, we should have the invariant + // that there are no `specialize` instructions inside of non-generic + // functions that in turn reference a generic type/function unless the generic is for a + // builtin type/function, or some of the type arguments are unknown at compile time, in + // which case we will rely on a follow up pass the translate it into a dynamic dispatch + // function. + // + // Now we consider lower lookupWitnessMethod insts into dynamic dispatch calls, + // which may open up more specialization opportunities. + // + iterChanged = lowerWitnessLookup(module, sink); + if (!iterChanged || sink->getErrorCount()) + break; } - // Once the work list has gone dry, we should have the invariant - // that there are no `specialize` instructions inside of non-generic - // functions that in turn reference a generic type/function unless the generic is for a - // builtin type/function, or some of the type arguments are unknown at compile time, in - // which case we will rely on a follow up pass the translate it into a dynamic dispatch - // function. // For functions that still have `specialize` uses left, we need to preserve the // its specializations in resulting IR so they can be reconstructed when this @@ -964,16 +839,13 @@ struct SpecializationContext writeSpecializationDictionaries(); } - void addDirtyInstsToWorkListRec(IRInst* inst) + void addInstsToWorkListRec(IRInst* inst) { - if (!cleanInsts.contains(inst)) - { - addToWorkList(inst); - } + addToWorkList(inst); for (auto child = inst->getLastChild(); child; child = child->getPrevInst()) { - addDirtyInstsToWorkListRec(child); + addInstsToWorkListRec(child); } } @@ -1323,11 +1195,14 @@ struct SpecializationContext } // Test if a type is compile time constant. - static bool isCompileTimeConstantType(IRInst* inst) + bool isCompileTimeConstantType(IRInst* inst) { // TODO: We probably need/want a more robust test here. // For now we are just look into the dependency graph of the inst and // see if there are any opcodes that are causing problems. + if (!isInstFullySpecialized(inst)) + return false; + List<IRInst*> localWorkList; HashSet<IRInst*> processedInsts; localWorkList.add(inst); @@ -1558,12 +1433,6 @@ struct SpecializationContext IRFunc* newFunc = builder->createFunc(); newFunc->setFullType(newFuncType); - // By construction, our new function type will be - // "fully specialized" by the rules used for doing - // generic specialization elsewhere in this pass. - // - fullySpecializedInsts.add(newFuncType); - // The above steps have accomplished the "first phase" // of cloning the function (since `IRFunc`s have no // operands). |
