mirror of
https://github.com/apple/swift.git
synced 2025-12-21 12:14:44 +01:00
AliasAnalysis: make AliasAnalysis a function analysis and simplify the cache keys
Instead of caching alias results globally for the module, make AliasAnalysis a FunctionAnalysisBase which caches the alias results per function.
Why?
* So far the result caches could only grow. They were reset when they reached a certain size. This was not ideal. Now, they are invalidated whenever the function changes.
* It was not possible to actually invalidate an alias analysis result. This is required, for example in TempRValueOpt and TempLValueOpt (so far it was done manually with invalidateInstruction).
* Type based alias analysis results were also cached for the whole module, while it is actually dependent on the function, because it depends on the function's resilience expansion. This was a potential bug.
I also added a new PassManager API to directly get a function-base analysis:
getAnalysis(SILFunction *f)
The second change of this commit is the removal of the instruction-index indirection for the cache keys. Now the cache keys directly work on instruction pointers instead of instruction indices. This reduces the number of hash table lookups for a cache lookup from 3 to 1.
This indirection was needed to avoid dangling instruction pointers in the cache keys. But this is not needed anymore, because of the new delayed instruction deletion mechanism.
This commit is contained in:
@@ -1189,7 +1189,7 @@ public:
|
||||
POA->invalidateFunction(F);
|
||||
|
||||
auto *PO = POA->get(F);
|
||||
auto *AA = PM->getAnalysis<AliasAnalysis>();
|
||||
auto *AA = PM->getAnalysis<AliasAnalysis>(F);
|
||||
auto *RCFI = PM->getAnalysis<RCIdentityAnalysis>()->get(F);
|
||||
|
||||
llvm::SpecificBumpPtrAllocator<BlockState> BPA;
|
||||
|
||||
@@ -89,7 +89,7 @@ void COWOptsPass::run() {
|
||||
LLVM_DEBUG(llvm::dbgs() << "*** RedundantPhiElimination on function: "
|
||||
<< F->getName() << " ***\n");
|
||||
|
||||
AA = PM->getAnalysis<AliasAnalysis>();
|
||||
AA = PM->getAnalysis<AliasAnalysis>(F);
|
||||
|
||||
bool changed = false;
|
||||
for (SILBasicBlock &block : *F) {
|
||||
|
||||
@@ -1275,7 +1275,7 @@ public:
|
||||
LLVM_DEBUG(llvm::dbgs() << "*** DSE on function: " << F->getName()
|
||||
<< " ***\n");
|
||||
|
||||
auto *AA = PM->getAnalysis<AliasAnalysis>();
|
||||
auto *AA = PM->getAnalysis<AliasAnalysis>(F);
|
||||
auto *TE = PM->getAnalysis<TypeExpansionAnalysis>();
|
||||
auto *EAFI = PM->getAnalysis<EpilogueARCAnalysis>()->get(F);
|
||||
|
||||
|
||||
@@ -1681,7 +1681,7 @@ public:
|
||||
LLVM_DEBUG(llvm::dbgs() << "*** RLE on function: " << F->getName()
|
||||
<< " ***\n");
|
||||
|
||||
auto *AA = PM->getAnalysis<AliasAnalysis>();
|
||||
auto *AA = PM->getAnalysis<AliasAnalysis>(F);
|
||||
auto *TE = PM->getAnalysis<TypeExpansionAnalysis>();
|
||||
auto *PO = PM->getAnalysis<PostOrderAnalysis>()->get(F);
|
||||
auto *EAFI = PM->getAnalysis<EpilogueARCAnalysis>()->get(F);
|
||||
|
||||
@@ -1778,7 +1778,7 @@ public:
|
||||
if (F->hasOwnership())
|
||||
return;
|
||||
|
||||
auto *AA = getAnalysis<AliasAnalysis>();
|
||||
auto *AA = getAnalysis<AliasAnalysis>(F);
|
||||
auto *PO = getAnalysis<PostOrderAnalysis>()->get(F);
|
||||
auto *RCIA = getAnalysis<RCIdentityAnalysis>()->get(getFunction());
|
||||
|
||||
|
||||
@@ -78,10 +78,8 @@ public:
|
||||
void run() override;
|
||||
|
||||
private:
|
||||
AliasAnalysis *AA = nullptr;
|
||||
|
||||
bool tempLValueOpt(CopyAddrInst *copyInst);
|
||||
bool combineCopyAndDestroy(CopyAddrInst *copyInst);
|
||||
void tempLValueOpt(CopyAddrInst *copyInst);
|
||||
void combineCopyAndDestroy(CopyAddrInst *copyInst);
|
||||
};
|
||||
|
||||
void TempLValueOptPass::run() {
|
||||
@@ -92,9 +90,6 @@ void TempLValueOptPass::run() {
|
||||
LLVM_DEBUG(llvm::dbgs() << "*** TempLValueOptPass on function: "
|
||||
<< F->getName() << " ***\n");
|
||||
|
||||
AA = PM->getAnalysis<AliasAnalysis>();
|
||||
|
||||
bool changed = false;
|
||||
for (SILBasicBlock &block : *F) {
|
||||
// First collect all copy_addr instructions upfront to avoid iterator
|
||||
// invalidation problems (the optimizations might delete the copy_addr
|
||||
@@ -106,14 +101,10 @@ void TempLValueOptPass::run() {
|
||||
}
|
||||
// Do the optimizations.
|
||||
for (CopyAddrInst *copyInst : copyInsts) {
|
||||
changed |= combineCopyAndDestroy(copyInst);
|
||||
changed |= tempLValueOpt(copyInst);
|
||||
combineCopyAndDestroy(copyInst);
|
||||
tempLValueOpt(copyInst);
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
invalidateAnalysis(SILAnalysis::InvalidationKind::Instructions);
|
||||
}
|
||||
}
|
||||
|
||||
static SingleValueInstruction *isMovableProjection(SILValue addr) {
|
||||
@@ -129,7 +120,7 @@ static SingleValueInstruction *isMovableProjection(SILValue addr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool TempLValueOptPass::tempLValueOpt(CopyAddrInst *copyInst) {
|
||||
void TempLValueOptPass::tempLValueOpt(CopyAddrInst *copyInst) {
|
||||
|
||||
// An overview of the algorithm:
|
||||
//
|
||||
@@ -147,11 +138,11 @@ bool TempLValueOptPass::tempLValueOpt(CopyAddrInst *copyInst) {
|
||||
// 'destroy_addr %destination' is inserted before the first use of %temp.
|
||||
|
||||
if (!copyInst->isTakeOfSrc())
|
||||
return false;
|
||||
return;
|
||||
|
||||
auto *temporary = dyn_cast<AllocStackInst>(copyInst->getSrc());
|
||||
if (!temporary)
|
||||
return false;
|
||||
return;
|
||||
|
||||
// Collect all users of the temporary into a set. Also, for simplicity,
|
||||
// require that all users are within a single basic block.
|
||||
@@ -160,7 +151,7 @@ bool TempLValueOptPass::tempLValueOpt(CopyAddrInst *copyInst) {
|
||||
for (Operand *use : temporary->getUses()) {
|
||||
SILInstruction *user = use->getUser();
|
||||
if (user->getParent() != block)
|
||||
return false;
|
||||
return;
|
||||
users.insert(user);
|
||||
}
|
||||
|
||||
@@ -176,7 +167,7 @@ bool TempLValueOptPass::tempLValueOpt(CopyAddrInst *copyInst) {
|
||||
// Just to be on the safe side, bail if it's not the case (instead of an
|
||||
// assert).
|
||||
if (users.count(projInst))
|
||||
return false;
|
||||
return;
|
||||
projections.insert(projInst);
|
||||
destRootAddr = projInst->getOperand(0);
|
||||
}
|
||||
@@ -185,6 +176,7 @@ bool TempLValueOptPass::tempLValueOpt(CopyAddrInst *copyInst) {
|
||||
SILInstruction *destRootInst = destRootAddr->getDefiningInstruction();
|
||||
|
||||
// Iterate over the liferange of the temporary and make some validity checks.
|
||||
AliasAnalysis *AA = nullptr;
|
||||
SILInstruction *beginOfLiferange = nullptr;
|
||||
bool endOfLiferangeReached = false;
|
||||
for (auto iter = temporary->getIterator(); iter != block->end(); ++iter) {
|
||||
@@ -197,7 +189,7 @@ bool TempLValueOptPass::tempLValueOpt(CopyAddrInst *copyInst) {
|
||||
// Check if the copyInst is the last user of the temporary (beside the
|
||||
// dealloc_stack).
|
||||
if (endOfLiferangeReached)
|
||||
return false;
|
||||
return;
|
||||
|
||||
// Find the first user of the temporary to get a more precise liferange.
|
||||
// It would be too conservative to treat the alloc_stack itself as the
|
||||
@@ -213,8 +205,11 @@ bool TempLValueOptPass::tempLValueOpt(CopyAddrInst *copyInst) {
|
||||
// temporary, we cannot replace all uses of the temporary with the
|
||||
// destination (it would break the def-use dominance rule).
|
||||
if (inst == destRootInst)
|
||||
return false;
|
||||
return;
|
||||
|
||||
if (!AA)
|
||||
AA = PM->getAnalysis<AliasAnalysis>(getFunction());
|
||||
|
||||
// Check if the destination is not accessed within the liferange of
|
||||
// the temporary.
|
||||
// This is unlikely, because the destination is initialized at the
|
||||
@@ -223,7 +218,7 @@ bool TempLValueOptPass::tempLValueOpt(CopyAddrInst *copyInst) {
|
||||
if (AA->mayReadOrWriteMemory(inst, destination) &&
|
||||
// Needed to treat init_existential_addr as not-writing projection.
|
||||
projections.count(inst) == 0)
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
assert(endOfLiferangeReached);
|
||||
@@ -253,18 +248,17 @@ bool TempLValueOptPass::tempLValueOpt(CopyAddrInst *copyInst) {
|
||||
user->eraseFromParent();
|
||||
break;
|
||||
default:
|
||||
AA->invalidateInstruction(user);
|
||||
use->set(destination);
|
||||
}
|
||||
}
|
||||
temporary->eraseFromParent();
|
||||
copyInst->eraseFromParent();
|
||||
return true;
|
||||
invalidateAnalysis(SILAnalysis::InvalidationKind::Instructions);
|
||||
}
|
||||
|
||||
bool TempLValueOptPass::combineCopyAndDestroy(CopyAddrInst *copyInst) {
|
||||
void TempLValueOptPass::combineCopyAndDestroy(CopyAddrInst *copyInst) {
|
||||
if (copyInst->isTakeOfSrc())
|
||||
return false;
|
||||
return;
|
||||
|
||||
// Find a destroy_addr of the copy's source address.
|
||||
DestroyAddrInst *destroy = nullptr;
|
||||
@@ -274,7 +268,7 @@ bool TempLValueOptPass::combineCopyAndDestroy(CopyAddrInst *copyInst) {
|
||||
}
|
||||
SILBasicBlock *block = copyInst->getParent();
|
||||
if (!destroy || destroy->getParent() != block)
|
||||
return false;
|
||||
return;
|
||||
assert(destroy->getOperand() == copyInst->getSrc());
|
||||
|
||||
// Check if the destroy_addr is after the copy_addr and if there are no
|
||||
@@ -290,16 +284,16 @@ bool TempLValueOptPass::combineCopyAndDestroy(CopyAddrInst *copyInst) {
|
||||
for (auto debugInst : debugInsts) {
|
||||
debugInst->eraseFromParent();
|
||||
}
|
||||
return true;
|
||||
invalidateAnalysis(SILAnalysis::InvalidationKind::Instructions);
|
||||
return;
|
||||
}
|
||||
if (auto *debugInst = dyn_cast<DebugValueAddrInst>(inst)) {
|
||||
if (debugInst->getOperand() == copyInst->getSrc())
|
||||
debugInsts.push_back(debugInst);
|
||||
}
|
||||
if (inst->mayReadOrWriteMemory())
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // end anonymous namespace
|
||||
|
||||
@@ -75,8 +75,6 @@ namespace {
|
||||
///
|
||||
/// TODO: Check if we still need to handle stores when RLE supports OSSA.
|
||||
class TempRValueOptPass : public SILFunctionTransform {
|
||||
AliasAnalysis *aa = nullptr;
|
||||
|
||||
bool collectLoads(Operand *addressUse, CopyAddrInst *originalCopy,
|
||||
SmallPtrSetImpl<SILInstruction *> &loadInsts);
|
||||
bool collectLoadsFromProjection(SingleValueInstruction *projection,
|
||||
@@ -84,16 +82,17 @@ class TempRValueOptPass : public SILFunctionTransform {
|
||||
SmallPtrSetImpl<SILInstruction *> &loadInsts);
|
||||
|
||||
SILInstruction *getLastUseWhileSourceIsNotModified(
|
||||
CopyAddrInst *copyInst, const SmallPtrSetImpl<SILInstruction *> &useInsts);
|
||||
CopyAddrInst *copyInst, const SmallPtrSetImpl<SILInstruction *> &useInsts,
|
||||
AliasAnalysis *aa);
|
||||
|
||||
bool
|
||||
checkTempObjectDestroy(AllocStackInst *tempObj, CopyAddrInst *copyInst);
|
||||
|
||||
bool extendAccessScopes(CopyAddrInst *copyInst, SILInstruction *lastUseInst);
|
||||
bool extendAccessScopes(CopyAddrInst *copyInst, SILInstruction *lastUseInst,
|
||||
AliasAnalysis *aa);
|
||||
|
||||
bool tryOptimizeCopyIntoTemp(CopyAddrInst *copyInst);
|
||||
std::pair<SILBasicBlock::iterator, bool>
|
||||
tryOptimizeStoreIntoTemp(StoreInst *si);
|
||||
void tryOptimizeCopyIntoTemp(CopyAddrInst *copyInst);
|
||||
SILBasicBlock::iterator tryOptimizeStoreIntoTemp(StoreInst *si);
|
||||
|
||||
void run() override;
|
||||
};
|
||||
@@ -306,7 +305,8 @@ collectLoads(Operand *addressUse, CopyAddrInst *originalCopy,
|
||||
/// of the temporary and look for the last use, which effectively ends the
|
||||
/// lifetime.
|
||||
SILInstruction *TempRValueOptPass::getLastUseWhileSourceIsNotModified(
|
||||
CopyAddrInst *copyInst, const SmallPtrSetImpl<SILInstruction *> &useInsts) {
|
||||
CopyAddrInst *copyInst, const SmallPtrSetImpl<SILInstruction *> &useInsts,
|
||||
AliasAnalysis *aa) {
|
||||
if (useInsts.empty())
|
||||
return copyInst;
|
||||
unsigned numLoadsFound = 0;
|
||||
@@ -358,7 +358,7 @@ SILInstruction *TempRValueOptPass::getLastUseWhileSourceIsNotModified(
|
||||
/// We must not replace %temp with %a after the end_access. Instead we try to
|
||||
/// move the end_access after "use %temp".
|
||||
bool TempRValueOptPass::extendAccessScopes(
|
||||
CopyAddrInst *copyInst, SILInstruction *lastUseInst) {
|
||||
CopyAddrInst *copyInst, SILInstruction *lastUseInst, AliasAnalysis *aa) {
|
||||
|
||||
SILValue copySrc = copyInst->getSrc();
|
||||
EndAccessInst *endAccessToMove = nullptr;
|
||||
@@ -460,13 +460,13 @@ bool TempRValueOptPass::checkTempObjectDestroy(
|
||||
}
|
||||
|
||||
/// Tries to perform the temporary rvalue copy elimination for \p copyInst
|
||||
bool TempRValueOptPass::tryOptimizeCopyIntoTemp(CopyAddrInst *copyInst) {
|
||||
void TempRValueOptPass::tryOptimizeCopyIntoTemp(CopyAddrInst *copyInst) {
|
||||
if (!copyInst->isInitializationOfDest())
|
||||
return false;
|
||||
return;
|
||||
|
||||
auto *tempObj = dyn_cast<AllocStackInst>(copyInst->getDest());
|
||||
if (!tempObj)
|
||||
return false;
|
||||
return;
|
||||
|
||||
bool isOSSA = copyInst->getFunction()->hasOwnership();
|
||||
|
||||
@@ -502,21 +502,23 @@ bool TempRValueOptPass::tryOptimizeCopyIntoTemp(CopyAddrInst *copyInst) {
|
||||
// Otherwise we would risk of inserting the destroy too early.
|
||||
// So we just treat the destroy_addr as any other use of tempObj.
|
||||
if (user->getParent() != copyInst->getParent())
|
||||
return false;
|
||||
return;
|
||||
loadInsts.insert(user);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!collectLoads(useOper, copyInst, loadInsts))
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
AliasAnalysis *aa = getPassManager()->getAnalysis<AliasAnalysis>(getFunction());
|
||||
|
||||
// Check if the source is modified within the lifetime of the temporary.
|
||||
SILInstruction *lastLoadInst = getLastUseWhileSourceIsNotModified(copyInst,
|
||||
loadInsts);
|
||||
SILInstruction *lastLoadInst =
|
||||
getLastUseWhileSourceIsNotModified(copyInst, loadInsts, aa);
|
||||
if (!lastLoadInst)
|
||||
return false;
|
||||
return;
|
||||
|
||||
// We cannot insert the destroy of copySrc after lastLoadInst if copySrc is
|
||||
// re-initialized by exactly this instruction.
|
||||
@@ -527,13 +529,13 @@ bool TempRValueOptPass::tryOptimizeCopyIntoTemp(CopyAddrInst *copyInst) {
|
||||
if (needToInsertDestroy && lastLoadInst != copyInst &&
|
||||
!isa<DestroyAddrInst>(lastLoadInst) &&
|
||||
aa->mayWriteToMemory(lastLoadInst, copySrc))
|
||||
return false;
|
||||
return;
|
||||
|
||||
if (!isOSSA && !checkTempObjectDestroy(tempObj, copyInst))
|
||||
return false;
|
||||
return;
|
||||
|
||||
if (!extendAccessScopes(copyInst, lastLoadInst))
|
||||
return false;
|
||||
if (!extendAccessScopes(copyInst, lastLoadInst, aa))
|
||||
return;
|
||||
|
||||
LLVM_DEBUG(llvm::dbgs() << " Success: replace temp" << *tempObj);
|
||||
|
||||
@@ -556,7 +558,6 @@ bool TempRValueOptPass::tryOptimizeCopyIntoTemp(CopyAddrInst *copyInst) {
|
||||
while (!tempObj->use_empty()) {
|
||||
Operand *use = *tempObj->use_begin();
|
||||
SILInstruction *user = use->getUser();
|
||||
aa->invalidateInstruction(user);
|
||||
|
||||
switch (user->getKind()) {
|
||||
case SILInstructionKind::DestroyAddrInst:
|
||||
@@ -591,25 +592,25 @@ bool TempRValueOptPass::tryOptimizeCopyIntoTemp(CopyAddrInst *copyInst) {
|
||||
}
|
||||
|
||||
tempObj->eraseFromParent();
|
||||
return true;
|
||||
invalidateAnalysis(SILAnalysis::InvalidationKind::Instructions);
|
||||
}
|
||||
|
||||
std::pair<SILBasicBlock::iterator, bool>
|
||||
SILBasicBlock::iterator
|
||||
TempRValueOptPass::tryOptimizeStoreIntoTemp(StoreInst *si) {
|
||||
// If our store is an assign, bail.
|
||||
if (si->getOwnershipQualifier() == StoreOwnershipQualifier::Assign)
|
||||
return {std::next(si->getIterator()), false};
|
||||
return std::next(si->getIterator());
|
||||
|
||||
auto *tempObj = dyn_cast<AllocStackInst>(si->getDest());
|
||||
if (!tempObj) {
|
||||
return {std::next(si->getIterator()), false};
|
||||
return std::next(si->getIterator());
|
||||
}
|
||||
|
||||
// If our tempObj has a dynamic lifetime (meaning it is conditionally
|
||||
// initialized, conditionally taken, etc), we can not convert its uses to SSA
|
||||
// while eliminating it simply. So bail.
|
||||
if (tempObj->hasDynamicLifetime()) {
|
||||
return {std::next(si->getIterator()), false};
|
||||
return std::next(si->getIterator());
|
||||
}
|
||||
|
||||
// Scan all uses of the temporary storage (tempObj) to verify they all refer
|
||||
@@ -632,14 +633,14 @@ TempRValueOptPass::tryOptimizeStoreIntoTemp(StoreInst *si) {
|
||||
break;
|
||||
case SILInstructionKind::CopyAddrInst:
|
||||
if (cast<CopyAddrInst>(user)->getDest() == tempObj)
|
||||
return {std::next(si->getIterator()), false};
|
||||
return std::next(si->getIterator());
|
||||
break;
|
||||
case SILInstructionKind::MarkDependenceInst:
|
||||
if (cast<MarkDependenceInst>(user)->getValue() == tempObj)
|
||||
return {std::next(si->getIterator()), false};
|
||||
return std::next(si->getIterator());
|
||||
break;
|
||||
default:
|
||||
return {std::next(si->getIterator()), false};
|
||||
return std::next(si->getIterator());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -734,7 +735,8 @@ TempRValueOptPass::tryOptimizeStoreIntoTemp(StoreInst *si) {
|
||||
auto nextIter = std::next(si->getIterator());
|
||||
si->eraseFromParent();
|
||||
tempObj->eraseFromParent();
|
||||
return {nextIter, true};
|
||||
invalidateAnalysis(SILAnalysis::InvalidationKind::Instructions);
|
||||
return nextIter;
|
||||
}
|
||||
|
||||
//===----------------------------------------------------------------------===//
|
||||
@@ -746,9 +748,6 @@ void TempRValueOptPass::run() {
|
||||
LLVM_DEBUG(llvm::dbgs() << "Copy Peephole in Func "
|
||||
<< getFunction()->getName() << "\n");
|
||||
|
||||
aa = getPassManager()->getAnalysis<AliasAnalysis>();
|
||||
bool changed = false;
|
||||
|
||||
// Find all copy_addr instructions.
|
||||
llvm::SmallSetVector<CopyAddrInst *, 8> deadCopies;
|
||||
for (auto &block : *getFunction()) {
|
||||
@@ -759,13 +758,12 @@ void TempRValueOptPass::run() {
|
||||
if (auto *copyInst = dyn_cast<CopyAddrInst>(&*ii)) {
|
||||
// In case of success, this may delete instructions, but not the
|
||||
// CopyInst itself.
|
||||
changed |= tryOptimizeCopyIntoTemp(copyInst);
|
||||
tryOptimizeCopyIntoTemp(copyInst);
|
||||
// Remove identity copies which either directly result from successfully
|
||||
// calling tryOptimizeCopyIntoTemp or was created by an earlier
|
||||
// iteration, where another copy_addr copied the temporary back to the
|
||||
// source location.
|
||||
if (copyInst->getSrc() == copyInst->getDest()) {
|
||||
changed = true;
|
||||
deadCopies.insert(copyInst);
|
||||
}
|
||||
++ii;
|
||||
@@ -773,9 +771,7 @@ void TempRValueOptPass::run() {
|
||||
}
|
||||
|
||||
if (auto *si = dyn_cast<StoreInst>(&*ii)) {
|
||||
bool madeSingleChange;
|
||||
std::tie(ii, madeSingleChange) = tryOptimizeStoreIntoTemp(si);
|
||||
changed |= madeSingleChange;
|
||||
ii = tryOptimizeStoreIntoTemp(si);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -794,7 +790,6 @@ void TempRValueOptPass::run() {
|
||||
|
||||
DeadEndBlocks deBlocks(getFunction());
|
||||
for (auto *deadCopy : deadCopies) {
|
||||
assert(changed);
|
||||
auto *srcInst = deadCopy->getSrc()->getDefiningInstruction();
|
||||
deadCopy->eraseFromParent();
|
||||
// Simplify any access scope markers that were only used by the dead
|
||||
@@ -803,7 +798,7 @@ void TempRValueOptPass::run() {
|
||||
simplifyAndReplaceAllSimplifiedUsesAndErase(srcInst, callbacks, &deBlocks);
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
if (!deadCopies.empty()) {
|
||||
invalidateAnalysis(SILAnalysis::InvalidationKind::Instructions);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user