blob: d454493053f8f80f24b23bbd93fdccb9f54a8ddc [file] [log] [blame]
//===------ AccessEnforcementDom.cpp - dominated access removal opt -------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
///
/// This function pass removes dominated accesses in two ways:
///
/// 1) Remove accesses dominated by an existing access
///
/// General case:
/// begin_access A (may or may not have no_nested_conflict)
/// load/store
/// end_access
/// ...
/// begin_access A [no_nested_conflict] // dominated by the first access
/// load/store
/// end_access A
///
/// The second access scope does not need to be emitted.
///
/// 2) Add a new dominating accesses to loop's preheader
///
/// General case:
/// <loop preheader>
/// A = ref_element_addr
/// <loop>
/// begin_access A [dynamic] [no_nested_conflict]
///
/// Adding an empty begin_access A in the preheader would allow us to
/// turn the loop's access to [static]
///
/// Warning: This optimization requires that all points within this function
/// that begin an access can be identified. Failure to recognize the beginning
/// of an access scope could weaken dynamic enforcement.
///
/// FIXME: This pass currently only runs in the last-chance pipeline, with a
/// guarantee that no access marker removal is done after it. This happens to
/// work but is dangerous and violates SIL semantics. We should instead add a
/// flag for accesses to give them the semantics that they may guard memory
/// operations other than those enclosed by the access scope.
//===----------------------------------------------------------------------===//
#define DEBUG_TYPE "access-enforcement-dom"
#include "swift/SIL/DebugUtils.h"
#include "swift/SIL/MemAccessUtils.h"
#include "swift/SIL/SILBuilder.h"
#include "swift/SIL/SILFunction.h"
#include "swift/SILOptimizer/Analysis/DominanceAnalysis.h"
#include "swift/SILOptimizer/Analysis/LoopAnalysis.h"
#include "swift/SILOptimizer/PassManager/Transforms.h"
#include "swift/SILOptimizer/Utils/InstOptUtils.h"
#include "llvm/ADT/DepthFirstIterator.h"
using namespace swift;
// =============================================================================
// DominatedAccessAnalysis
namespace swift {
/// Information about each dynamic access with valid storage.
///
/// This is a pass-specific subclass of AccessedStorage with identical layout.
/// An instance is created for each BeginAccess in the current function. In
/// additional to identifying the access' storage location, it associates that
/// access with pass-specific data in reserved bits. The reserved bits do not
/// participate in equality or hash lookup.
class DomAccessedStorage : public AccessedStorage {
public:
DomAccessedStorage() {}
explicit DomAccessedStorage(const AccessedStorage &storage)
: AccessedStorage(storage) {
Bits.DomAccessedStorage.isInner = false;
Bits.DomAccessedStorage.containsRead = false;
}
// Is this dynamic, identifiable access scope potentially contained within any
// kind of outer scope. The outer scope may be static and/or have unidentified
// storage. For example:
//
// A1: [static] [read]
// A2: [dynamic] [read]
// A3: [dynamic] [modify]
//
// A2 cannot be promoted to modify if its scope overlaps with A1.
bool isInner() const { return Bits.DomAccessedStorage.isInner; }
void setIsInner() { Bits.DomAccessedStorage.isInner = true; }
/// Is this dynamic, identifiable access scope a [read], and does it
/// potentially contain another non-distinct [read] access of any kind?
bool containsRead() const { return Bits.DomAccessedStorage.containsRead; }
void setContainsRead() { Bits.DomAccessedStorage.containsRead = true; }
void dump() const {
AccessedStorage::dump();
llvm::dbgs() << "<" << (isInner() ? "" : "inner")
<< (containsRead() ? "" : "containsRead") << ">\n";
}
};
} // namespace swift
namespace {
// An analysis that maps each valid dynamic BeginAccess to
// DomAccessedStorage. Performs a trivial data flow analysis to populate the map
// with information about nested accesses. Data flow is needed to track open
// access scopes, but only flow-insensitive information is recorded in the
// result.
//
// Note that all access scopes are tracked during data flow, but only valid
// dynamic are mapped to results.
//
// TODO: Separate this into a shared analysis and factor it with
// AccessEnforcementOpts. The optimization that merges accesses would also
// benefit.
//
// TODO: This could be made more precise by querying AccessedStorageAnalysis.
class DominatedAccessAnalysis {
public:
// The result records information for all dynamic accesses in this
// function. If an UnpairedAccess exists, then the result will be
// consevatively empty.
struct Result {
llvm::SmallDenseMap<BeginAccessInst *, DomAccessedStorage, 32> accessMap;
};
private:
// The data flow state for each block. It is only valid between the time at
// least one predecesor block has been finished and before its own block
// has been finished.
//
// The isBottom flag allows the analysis to avoid quadratic behavior where
// each open access must be updated at each conservatively handled
// instruction. Instead, the accesses are only conservatively updated once
// until the next scope is entered. So the complexity is (#OpenScopes)^2
// instead of (#OpenScopes)*(#Applies).
struct BBState {
using DenseAccessSet = llvm::SmallDenseSet<BeginAccessInst *, 4>;
using DenseCoroutineSet = llvm::SmallDenseSet<BeginApplyInst *, 4>;
DenseAccessSet inScopeAccesses;
DenseCoroutineSet inScopeCoroutines;
bool isBottom = false;
};
using BlockStateMap = llvm::DenseMap<SILBasicBlock *, BBState>;
PostOrderFunctionInfo *PO;
Result result; // Flow-insensitive analysis result.
BlockStateMap blockStateMap; // Data flow state.
public:
DominatedAccessAnalysis(PostOrderFunctionInfo *PO) : PO(PO) {}
Result analyze() &&;
protected:
void setBottom(BBState &state);
void analyzeAccess(BeginAccessInst *BAI, BBState &state);
};
} // namespace
// Set information for all in-scope accesses to the worst case "bottom".
// The isInner flag is not affected because it only applies to scopes inside the
// currently open scopes, not the currently open scopes themselves.
void DominatedAccessAnalysis::setBottom(BBState &state) {
if (state.isBottom)
return;
// Unordered iteration over the in-access scopes.
llvm::for_each(state.inScopeAccesses, [this](BeginAccessInst *BAI) {
if (auto &domStorage = result.accessMap[BAI])
domStorage.setContainsRead();
});
}
// Perform the analysis and return the Result, relinquishing internal state.
DominatedAccessAnalysis::Result DominatedAccessAnalysis::analyze() && {
// A single RPO traversal is sufficient to visit access scopes in order. The
// set of open accesses entering a block cannot grow as a result of loops, and
// within an open scope the results are flow-insensitive.
for (auto *BB : PO->getReversePostOrder()) {
BBState state = blockStateMap[BB];
for (auto &I : *BB) {
if (auto *BAI = dyn_cast<BeginAccessInst>(&I)) {
analyzeAccess(BAI, state);
continue;
}
if (auto *EAI = dyn_cast<EndAccessInst>(&I)) {
bool erased = state.inScopeAccesses.erase(EAI->getBeginAccess());
(void)erased;
assert(erased);
continue;
}
// Check for BeginApply before checking FullApplySite below.
if (auto *beginApply = dyn_cast<BeginApplyInst>(&I)) {
auto iterAndInserted = state.inScopeCoroutines.insert(beginApply);
(void)iterAndInserted;
assert(iterAndInserted.second);
continue;
}
if (auto *endApply = dyn_cast<EndApplyInst>(&I)) {
bool erased = state.inScopeCoroutines.erase(endApply->getBeginApply());
(void)erased;
assert(erased);
continue;
}
if (FullApplySite::isa(&I)) {
setBottom(state);
continue;
}
if (isa<BeginUnpairedAccessInst>(&I)) {
// Unpaired accesses could be tracked, but are ignored because they are
// mostly irrelevant and hard to test. Completely bail on this function.
result.accessMap.clear();
return std::move(result);
}
}
auto successors = BB->getTerminator()->getSuccessors();
unsigned numSucc = successors.size();
for (unsigned succIdx : indices(successors)) {
SILBasicBlock *succBB = successors[succIdx].getBB();
if (succBB == BB)
continue;
if (succIdx != numSucc - 1)
blockStateMap.try_emplace(succBB, state);
else
// Move the state into the last successor to avoid copying sets.
blockStateMap.try_emplace(succBB, std::move(state));
}
}
return std::move(result);
}
// The data flow transfer function for BeginAccess. Creates the
// DomAccessedStorage for this access and inserts it in the result.
void DominatedAccessAnalysis::analyzeAccess(BeginAccessInst *BAI,
BBState &state) {
DomAccessedStorage domStorage;
// Only track dynamic access in the result. Static accesses still need to be
// tracked by data flow, but they can't be optimized as "dominating".
if (BAI->getEnforcement() == SILAccessEnforcement::Dynamic) {
AccessedStorage storage = findAccessedStorageNonNested(BAI->getSource());
// Copy the AccessStorage into DomAccessedStorage. All pass-specific bits
// are initialized to zero.
domStorage = DomAccessedStorage(storage);
}
// Continue to handle both untracked access and invalid domStorage
// conservatively below...
// unordered set iteration...
llvm::for_each(state.inScopeAccesses, [&](BeginAccessInst *outerBegin) {
auto &outerInfo = result.accessMap[outerBegin];
// If the current access is mapped, set its isInner flag.
if (domStorage && !domStorage.isInner()) {
if (domStorage.isDistinctFrom(outerInfo))
return;
domStorage.setIsInner();
}
// The results for tracked in-scope accesses still need to be
// updated even if the current access is not be tracked.
if (outerInfo && BAI->getAccessKind() == SILAccessKind::Read
&& outerBegin->getAccessKind() == SILAccessKind::Read) {
outerInfo.setContainsRead();
}
});
// Track this access even if it is invalid or unmapped.
{
auto iterAndInserted = state.inScopeAccesses.insert(BAI);
(void)iterAndInserted;
assert(iterAndInserted.second);
}
// Update the results if this access will be mapped.
if (!domStorage)
return;
// Set the current accesss isInner flag if it's inside a coroutine scope.
if (!state.inScopeCoroutines.empty())
domStorage.setIsInner();
// Map the current access.
{
auto iterAndInserted = result.accessMap.try_emplace(BAI, domStorage);
(void)iterAndInserted;
assert(iterAndInserted.second);
}
state.isBottom = false;
}
// =============================================================================
// DominatedAccessRemoval optimization.
namespace {
using DomTreeNode = llvm::DomTreeNodeBase<SILBasicBlock>;
// Visit the dominator tree top down, tracking the current set of dominating
// dynamic accesses. Dominated dynamic accesses with identical storage are
// marked static during traversal. If a dynamic access inside a loop has no
// dominating access, insert a new access in the preheader.
class DominatedAccessRemoval {
// Record the first access of a given storage location and the dominator node
// in which the access occurred.
struct DominatingAccess {
BeginAccessInst *beginAccess;
DomTreeNode *domNode;
DominatingAccess(BeginAccessInst *beginAccess, DomTreeNode *domNode)
: beginAccess(beginAccess), domNode(domNode) {}
};
using StorageToDomMap = llvm::DenseMap<AccessedStorage, DominatingAccess>;
SILFunction &func;
DominanceInfo *domInfo;
SILLoopInfo *loopInfo;
DominatedAccessAnalysis::Result &DAA;
// Hash map from each storage location to the dominating access.
StorageToDomMap storageToDomMap;
DomTreeNode *currDomNode = nullptr;
bool hasChanged = false;
public:
DominatedAccessRemoval(SILFunction &func, DominanceInfo *domInfo,
SILLoopInfo *loopInfo,
DominatedAccessAnalysis::Result &DAA)
: func(func), domInfo(domInfo), loopInfo(loopInfo), DAA(DAA) {}
bool optimize();
protected:
void visitBeginAccess(BeginAccessInst *BAI);
bool checkDominatedAccess(BeginAccessInst *BAI,
DomAccessedStorage currDomStorage);
bool optimizeDominatedAccess(BeginAccessInst *currBegin,
DomAccessedStorage currDomStorage,
const DominatingAccess &domAccess);
void tryInsertLoopPreheaderAccess(BeginAccessInst *BAI,
DomAccessedStorage currDomStorage);
};
} // namespace
// Optimize the current function, and return true if any optimization was
// performed.
bool DominatedAccessRemoval::optimize() {
DomTreeNode *entryNode = domInfo->getNode(func.getEntryBlock());
for (DomTreeNode *domNode : llvm::depth_first(entryNode)) {
currDomNode = domNode;
// Optimize dominated accesses in this block.
for (auto &instr : *domNode->getBlock()) {
if (auto *BAI = dyn_cast<BeginAccessInst>(&instr))
visitBeginAccess(BAI);
}
}
return hasChanged;
}
// Visit a BeginAccessInst once-and-only-once in domtree order.
// Attempt to find a dominating access with identical storage.
// If that fails, attempt to insert a new dominating access in the preheader.
void DominatedAccessRemoval::visitBeginAccess(BeginAccessInst *BAI) {
if (BAI->getEnforcement() != SILAccessEnforcement::Dynamic)
return;
DomAccessedStorage currDomStorage = DAA.accessMap.lookup(BAI);
if (!currDomStorage)
return;
// Only track "identifiable" storage.
if (currDomStorage.isUniquelyIdentifiedOrClass()) {
if (checkDominatedAccess(BAI, currDomStorage))
return;
}
tryInsertLoopPreheaderAccess(BAI, currDomStorage);
}
// Track this identifiable dynamic access in storageToDomMap, and optimize it if
// possible. Return true if the optimization suceeds.
bool DominatedAccessRemoval::checkDominatedAccess(
BeginAccessInst *BAI, DomAccessedStorage currDomStorage) {
// Attempt to add this access to storageToDomMap using its base storage
// location as the key.
//
// Cast this DomAccessedStorage back to a plain storage location. The
// pass-specific bits will be ignored, but reset them anyway for sanity.
AccessedStorage storage = static_cast<AccessedStorage>(currDomStorage);
storage.resetSubclassData();
auto iterAndInserted =
storageToDomMap.try_emplace(storage, DominatingAccess(BAI, currDomNode));
if (iterAndInserted.second)
return false;
// An access has already been recorded for this storage.
// If the previous domNode does not dominate currDomNode, then replace it.
DominatingAccess &domAccess = iterAndInserted.first->second;
if (!domInfo->dominates(domAccess.domNode, currDomNode)) {
domAccess = DominatingAccess(BAI, currDomNode);
return false;
}
// The previously mapped access still dominates this block, so the current
// access can potentially be optimized.
return optimizeDominatedAccess(BAI, currDomStorage, domAccess);
}
// If possible, optimize the current access by converting it to [static]. Return
// true if the optimization succeeds.
//
// This function is not allowed to add or erase instructions, only change
// instruction flags.
//
// The four required conditions for converting this access to static are:
//
// 1. A closed dominating access has identical storage.
//
// The caller looked up this access' storage in storageToDomMap and checked
// that the previously seen access dominates this block. As long as this access'
// isInner flag from DominatedAccessAnalysis is not set, the dominating access
// must be closed.
//
// 2. There is no open (overlapping) access with nondistinct storage (that isn't
// also open at the dominating access).
//
// The isInner flag from DominatedAccessAnalysis indicates no enclosing
// nondistinct scopes within this function. Any outer scope would enclose the
// whole function, thereby also enclosing the dominating scope.
//
// 3. This access has no_nested_conflict.
//
// This is a direct check on this BeginAccessInst flag.
//
// 4. The current and dominating access kinds are compatible:
//
// read -> read: OK
//
// modify -> read: OK
//
// modify -> modify: OK
//
// read -> modify: Requires promoting the dominating access to a modify. This
// can be done as long as the dominating access does not contain another
// non-distinct read and isn't contained by another non-distinct read. The
// containsRead and isInner flags from in DominatedAccessAnalysis answer this
// conservatively.
//
// Note: Promoting an earlier access to a modify could cause a program to
// trap when optimized even if the unoptimized program does not trap; the
// original modify access may be on an unreachable code path. This is acceptable
// because:
//
// (a) in theory, exclusivity violations do not need to be executed to be
// considered program violations. Promoting the access does not introduce any
// new conflict where one didn't already exist statically. Catching these
// violations at runtime is only an implementation compromise, and the more true
// violations are caught, the better.
//
// (b) in practice, this situation is so exceedingly unlikely that it won't
// cause any pervasive usability problem where programs have stronger
// enforcement only when optimized.
bool DominatedAccessRemoval::optimizeDominatedAccess(
BeginAccessInst *BAI, DomAccessedStorage currAccessInfo,
const DominatingAccess &domAccess) {
// 1. and 2. If any nondistinct scopes are open, it must remain dynamic.
if (currAccessInfo.isInner())
return false;
// 3. If BAI may have a nested conflict, it must remain dynamic.
if (!BAI->hasNoNestedConflict())
return false;
// 4. Promoting a read to a modify is only safe with no nested reads.
if (domAccess.beginAccess->getAccessKind() == SILAccessKind::Read
&& BAI->getAccessKind() == SILAccessKind::Modify) {
DomAccessedStorage domStorage = DAA.accessMap[domAccess.beginAccess];
if (domStorage.containsRead() || domStorage.isInner())
return false;
LLVM_DEBUG(llvm::dbgs()
<< "Promoting to modify: " << *domAccess.beginAccess << "\n");
domAccess.beginAccess->setAccessKind(SILAccessKind::Modify);
}
LLVM_DEBUG(llvm::dbgs() << "Setting static enforcement: " << *BAI << "\n");
LLVM_DEBUG(llvm::dbgs() << "Dominated by: " << *domAccess.beginAccess
<< "\n");
BAI->setEnforcement(SILAccessEnforcement::Static);
hasChanged = true;
return true;
}
// Attempt to insert a new access in the loop preheader. If successful, insert
// the new access in DominatedAccessAnalysis so it can be used to dominate other
// accesses. Also convert the current access to static and update the current
// storageToDomMap since the access may already have been recorded (when it was
// still dynamic).
//
// This function cannot add or remove instructions in the current block, but
// may add instructions to the current loop's preheader.
//
// The required conditions for inserting a new dominating access are:
//
// 1. The new preheader access is not enclosed in another scope that doesn't
// also enclose the current scope.
//
// This is inferred from the loop structure; any scope that encloses the
// preheader must also enclose the entire loop.
//
// 2. The current access is not enclosed in another scope that doesn't also
// enclose the preheader.
//
// As before, it is sufficient to check this access' isInner flags in
// DominatedAccessAnalysis; if this access isn't enclosed by any scope within
// the function, then it can't be enclosed within a scope inside the loop.
//
// 3. The current header has no nested conflict within its scope.
//
// 4. The access' source operand is available in the loop preheader.
void DominatedAccessRemoval::tryInsertLoopPreheaderAccess(
BeginAccessInst *BAI, DomAccessedStorage currAccessInfo) {
// 2. the current access may be enclosed.
if (currAccessInfo.isInner())
return;
// 3. the current access must be instantaneous.
if (!BAI->hasNoNestedConflict())
return;
SILLoop *currLoop = loopInfo->getLoopFor(BAI->getParent());
if (!currLoop)
return;
SILBasicBlock *preheader = currLoop->getLoopPreheader();
if (!preheader)
return;
// 4. The source operand must be available in the preheader.
auto sourceOperand = BAI->getOperand();
auto *sourceBB = sourceOperand->getParentBlock();
if (!domInfo->dominates(sourceBB, preheader))
return;
// Insert a new access scope immediately before the
// preheader's terminator.
TermInst *preheaderTerm = preheader->getTerminator();
SILBuilderWithScope scopeBuilder(preheaderTerm);
BeginAccessInst *newBegin = scopeBuilder.createBeginAccess(
preheaderTerm->getLoc(), sourceOperand, BAI->getAccessKind(),
SILAccessEnforcement::Dynamic, true /*no nested conflict*/,
BAI->isFromBuiltin());
scopeBuilder.createEndAccess(preheaderTerm->getLoc(), newBegin, false);
LLVM_DEBUG(llvm::dbgs() << "Created loop preheader access: " << *newBegin
<< "\n"
<< "dominating: " << *BAI << "\n");
BAI->setEnforcement(SILAccessEnforcement::Static);
hasChanged = true;
// Insert the new dominating instruction in both DominatedAccessAnalysis and
// storageToDomMap if it has uniquely identifiable storage.
if (!currAccessInfo.isUniquelyIdentifiedOrClass())
return;
AccessedStorage storage = static_cast<AccessedStorage>(currAccessInfo);
storage.resetSubclassData();
// Create a DomAccessedStorage for the new access with no flags set.
DAA.accessMap.try_emplace(newBegin, DomAccessedStorage(storage));
// Track the new access as long as no other accesses from the same storage are
// already tracked. This also necessarily replaces the current access, which
// was just made static.
DominatingAccess newDomAccess(newBegin, domInfo->getNode(preheader));
auto iterAndInserted = storageToDomMap.try_emplace(storage, newDomAccess);
if (!iterAndInserted.second) {
DominatingAccess &curDomAccess = iterAndInserted.first->second;
if (curDomAccess.beginAccess == BAI)
curDomAccess = newDomAccess;
}
}
namespace {
struct AccessEnforcementDom : public SILFunctionTransform {
void run() override;
};
} // namespace
void AccessEnforcementDom::run() {
SILFunction *func = getFunction();
if (func->empty())
return;
PostOrderFunctionInfo *PO = getAnalysis<PostOrderAnalysis>()->get(func);
auto DAA = DominatedAccessAnalysis(PO).analyze();
DominanceAnalysis *domAnalysis = getAnalysis<DominanceAnalysis>();
DominanceInfo *domInfo = domAnalysis->get(func);
SILLoopAnalysis *loopAnalysis = PM->getAnalysis<SILLoopAnalysis>();
SILLoopInfo *loopInfo = loopAnalysis->get(func);
DominatedAccessRemoval eliminationPass(*func, domInfo, loopInfo, DAA);
if (eliminationPass.optimize())
invalidateAnalysis(SILAnalysis::InvalidationKind::Instructions);
}
SILTransform *swift::createAccessEnforcementDom() {
return new AccessEnforcementDom();
}