| //===------ AccessEnforcementOpts.cpp - Optimize access enforcement -------===// |
| // |
| // This source file is part of the Swift.org open source project |
| // |
| // Copyright (c) 2014 - 2018 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 pass optimizes access enforcement as follows: |
| /// |
| /// Access marker folding: Find begin/end access scopes that are uninterrupted |
| /// by a potential conflicting access. Flag those as [nontracking] access. |
| /// |
| /// Folding must prove that no dynamic conflicts occur inside of an access |
| /// scope. That is, a scope has no "nested inner conflicts". The access itself |
| /// may still conflict with an outer scope. If successful, folding simply sets |
| /// the [no_nested_conflict] attribute on the begin_[unpaired_]access |
| /// instruction and removes all corresponding end_[unpaired_]access |
| /// instructions. |
| /// |
| /// This analysis is conceptually similar to DiagnoseStaticExclusivity. The |
| /// difference is that it conservatively considers any dynamic access that may |
| /// alias, as opposed to only the obviously aliasing accesses (it is the |
| /// complement of the static diagnostic pass in that respect). This makes a |
| /// considerable difference in the implementation. For example, |
| /// DiagnoseStaticExclusivity must be able to fully analyze all @inout_aliasable |
| /// parameters because they aren't dynamically enforced. This optimization |
| /// completely ignores @inout_aliasable paramters because it only cares about |
| /// dynamic enforcement. This optimization also does not attempt to look at |
| /// subaccess paths, because it should not weaken enforcement in any way--a |
| /// program that traps at -Onone should also trap at -O. |
| /// |
| /// AccessFolding is a forward data flow analysis that tracks open accesses. If |
| /// any path to an access' end of scope has a potentially conflicting access, |
| /// then that access is marked as a nested conflict. |
| /// |
| /// Pass order dependencies: |
| /// - Benefits from running after AccessEnforcementSelection. |
| /// |
| /// TODO: Perform another run of AccessEnforcementSelection immediately before |
| /// this pass. Currently, that pass only works well when run before |
| /// AllocBox2Stack. Ideally all such closure analysis passes are combined into a |
| /// shared anlysis with a set of associated optimizations that can be rerun at |
| /// any point in the pipeline. Until then, we could settle for a partially |
| /// working AccessEnforcementSelection, or expand it somewhat to handle |
| /// alloc_stack. |
| /// |
| /// TODO: Add test case. Use SideEffectAnalysis to ignore calls without global |
| /// effects. Also scan the arguments to see if there are closure captures. If |
| /// this isn't sufficient, add a bit to SideEffectAnalysis for NonlocalAccess. |
| /// |
| /// TODO: Add test case. For Local variables. Ignore global side effects if |
| /// none of the intervening calls can access the captured variable. |
| /// |
| /// TODO: Add test case. For Class/Global variables. Ignore global side effects |
| /// if callee analysis and interprocedural analysis tell us that none of the |
| /// intervening calls access that variable. |
| /// |
| /// TODO: As a separate module pass, find global/class accesses to properties |
| /// that are never passed "reentrantly". That optmimization could make use of |
| /// the no_nested_conflict flags set in this pass. If *none* of a module-private |
| /// variable's accesses have nested conflict, then they can be downgraded to |
| /// static checks en masse. |
| /// |
| //===----------------------------------------------------------------------===// |
| |
| #define DEBUG_TYPE "access-enforcement-opts" |
| |
| #include "swift/SIL/DebugUtils.h" |
| #include "swift/SIL/MemAccessUtils.h" |
| #include "swift/SIL/SILFunction.h" |
| #include "swift/SILOptimizer/Analysis/AccessedStorageAnalysis.h" |
| #include "swift/SILOptimizer/PassManager/Transforms.h" |
| #include "swift/SILOptimizer/Utils/Local.h" |
| #include "llvm/ADT/SmallBitVector.h" |
| |
| using namespace swift; |
| |
| namespace swift { |
| /// A value-based subclass of AccessedStorage with identical layout. This |
| /// provides access to pass-specific data in reserved bits. |
| /// |
| /// The fully-qualified class name allows for the declaration of bitfields in |
| /// AccessedStorage. In this file, it is aliased to AccessInfo. |
| class AccessEnforcementOptsInfo : public AccessedStorage { |
| public: |
| /// Get a unique index for this access within its function. |
| unsigned getAccessIndex() const { |
| return Bits.AccessEnforcementOptsInfo.beginAccessIndex; |
| } |
| |
| void setAccessIndex(unsigned index) { |
| Bits.AccessEnforcementOptsInfo.beginAccessIndex = index; |
| assert(unsigned(Bits.AccessEnforcementOptsInfo.beginAccessIndex) == index); |
| } |
| |
| /// Has the analysis seen a conflicting nested access on any path within this |
| /// access' scope. |
| bool seenNestedConflict() const { |
| return Bits.AccessEnforcementOptsInfo.seenNestedConflict; |
| } |
| |
| void setSeenNestedConflict() { |
| Bits.AccessEnforcementOptsInfo.seenNestedConflict = 1; |
| } |
| |
| void dump() const { |
| AccessedStorage::dump(); |
| llvm::dbgs() << " access index: " << getAccessIndex() << " <" |
| << (seenNestedConflict() ? "" : "no ") << "conflict>\n"; |
| } |
| }; |
| } // namespace swift |
| |
| using AccessInfo = AccessEnforcementOptsInfo; |
| |
| namespace { |
| class SparseAccessSet; |
| |
| /// A dense set of begin_[unpaired_]access instructions... because very few |
| /// accesses are typically in-progress at a particular program point, |
| /// particularly at block boundaries. |
| using DenseAccessSet = SmallVector<BeginAccessInst *, 4>; |
| |
| /// Perform the access folding optimization on a function. |
| /// |
| /// Maps each begin access instruction to its AccessInfo, which: |
| /// - identifies the accessed memory for conflict detection |
| /// - contains a pass-specific reachability set index |
| /// - contains a pass-specific flag that indicates the presence of a conflict |
| /// on any path. |
| /// |
| /// If, after computing reachability, an access' conflict flag is still not set, |
| /// then all paths in its scope are conflict free. Reachability begins at a |
| /// begin_[unpaired_]access instruction and ends either at a potential conflict |
| /// or at the end_[unpaired_]access instruction that is associated with the |
| /// begin access. |
| /// |
| /// Forward data flow computes `blockOutAccess` for each block. At a control |
| /// flow merge, this analysis forms an intersection of reachable accesses on |
| /// each path. Only visited predecessors are merged (unvisited paths |
| /// optimistically assume reachability). Before a block is visited, it has no |
| /// map entry in blockOutAccess. Blocks are processed in RPO order, and a single |
| /// begin_access dominates all associated end_access instructions. Consequently, |
| /// when a block is first visited, blockOutAccess contains the maximal |
| /// reachability set. Further iteration would only reduce this set. |
| /// |
| /// The only result of this analysis used by the optimization is the conflict |
| /// flag in AccessInfo. Since reducing a reachability set cannot further |
| /// detect conflicts, there is no need to iterate to a reachability fix point. |
| class AccessFolding { |
| friend class SparseAccessSet; |
| |
| SILFunction *F; |
| PostOrderFunctionInfo *PO; |
| AccessedStorageAnalysis *ASA; |
| |
| /// Map each begin access to its index, data, and flags. |
| llvm::SmallDenseMap<BeginAccessInst *, AccessInfo, 32> beginAccessMap; |
| |
| /// Tracks the in-progress accesses at the end of each block. |
| llvm::SmallDenseMap<SILBasicBlock *, DenseAccessSet, 32> blockOutAccess; |
| |
| public: |
| AccessFolding(SILFunction *F, PostOrderFunctionInfo *PO, |
| AccessedStorageAnalysis *ASA) |
| : F(F), PO(PO), ASA(ASA) {} |
| |
| void perform(); |
| |
| protected: |
| /// Get the AccessInfo for a BeginAccessInst within this function. All |
| /// accesses are mapped by identifyBeginAccesses(). |
| AccessInfo &getAccessInfo(BeginAccessInst *beginAccess) { |
| auto iter = beginAccessMap.find(beginAccess); |
| assert(iter != beginAccessMap.end()); |
| return iter->second; |
| } |
| const AccessInfo &getAccessInfo(BeginAccessInst *beginAccess) const { |
| return const_cast<AccessFolding &>(*this).getAccessInfo(beginAccess); |
| } |
| |
| /// Convenience. If AccessInfo has already been retrieved, get the index |
| /// directly from it instead of calling this to avoid additional hash lookup. |
| unsigned getAccessIndex(BeginAccessInst *beginAccess) const { |
| return getAccessInfo(beginAccess).getAccessIndex(); |
| } |
| |
| void identifyBeginAccesses(); |
| |
| void visitBeginAccess(BeginAccessInst *beginAccess, |
| SparseAccessSet &accessMap); |
| |
| void visitEndAccess(EndAccessInst *endAccess, SparseAccessSet &accessMap); |
| |
| void visitFullApply(FullApplySite fullApply, SparseAccessSet &accessMap); |
| |
| void mergePredAccesses(SILBasicBlock *succBB, |
| SparseAccessSet &mergedAccesses); |
| |
| void visitBlock(SILBasicBlock *BB); |
| |
| void foldNonNestedAccesses(); |
| }; |
| |
| /// A sparse set of in-flight accesses. |
| /// |
| /// Explodes a DenseAccessSet into a temporary sparse set for comparison |
| /// and membership. |
| /// |
| /// The AccessFolding pass provides the instruction to index mapping. Rather |
| /// than having all instances of SparseAccessSet reference the pass, it is only |
| /// passed in for the operations that require it. |
| class SparseAccessSet { |
| llvm::SmallBitVector bitmask; // Most functions have < 64 accesses. |
| DenseAccessSet denseVec; |
| |
| public: |
| /// Internally, the denseVec may contain entries that have been removed from |
| /// the bitmask. Iteration checks for membership in the bitmask. |
| class Iterator { |
| const AccessFolding &pass; |
| const SparseAccessSet &sparseSet; |
| DenseAccessSet::const_iterator denseIter; |
| |
| public: |
| Iterator(const SparseAccessSet &set, const AccessFolding &pass) |
| : pass(pass), sparseSet(set), denseIter(set.denseVec.begin()) {} |
| |
| BeginAccessInst *next() { |
| auto end = sparseSet.denseVec.end(); |
| while (denseIter != end) { |
| BeginAccessInst *beginAccess = *denseIter; |
| ++denseIter; |
| unsigned sparseIndex = pass.getAccessIndex(beginAccess); |
| if (sparseSet.bitmask[sparseIndex]) |
| return beginAccess; |
| } |
| return nullptr; |
| } |
| }; |
| |
| SparseAccessSet(AccessFolding &pass) : bitmask(pass.beginAccessMap.size()) {} |
| |
| SparseAccessSet(const DenseAccessSet &denseVec, AccessFolding &pass) |
| : bitmask(pass.beginAccessMap.size()), denseVec(denseVec) { |
| for (BeginAccessInst *beginAccess : denseVec) |
| bitmask.set(pass.getAccessIndex(beginAccess)); |
| } |
| |
| bool isEmpty(AccessFolding &pass) const { |
| Iterator iterator(*this, pass); |
| return iterator.next() == nullptr; |
| } |
| |
| bool contains(unsigned index) const { return bitmask[index]; } |
| |
| // Insert the given BeginAccessInst with its corresponding reachability index. |
| // Return true if the set was expanded. |
| bool insert(BeginAccessInst *beginAccess, unsigned index) { |
| if (bitmask[index]) |
| return false; |
| |
| bitmask.set(index); |
| denseVec.push_back(beginAccess); |
| return true; |
| } |
| |
| /// Erase an access from this set based on the index provided by its mapped |
| /// AccessInfo. |
| /// |
| /// Does not invalidate Iterator. |
| void erase(unsigned index) { bitmask.reset(index); } |
| |
| bool isEquivalent(const SparseAccessSet &other) const { |
| return bitmask == other.bitmask; |
| } |
| |
| // Only merge accesses that are present on the `other` map. i.e. erase |
| // all accesses in this map that are not present in `other`. |
| void merge(const SparseAccessSet &other) { bitmask &= other.bitmask; } |
| |
| void copyInto(DenseAccessSet &other, AccessFolding &pass) { |
| other.clear(); |
| Iterator iterator(*this, pass); |
| while (BeginAccessInst *beginAccess = iterator.next()) { |
| other.push_back(beginAccess); |
| } |
| } |
| |
| void dump(AccessFolding &pass) const { |
| Iterator iterator(*this, pass); |
| while (BeginAccessInst *beginAccess = iterator.next()) { |
| llvm::dbgs() << *beginAccess << " "; |
| pass.getAccessInfo(beginAccess).dump(); |
| } |
| } |
| }; |
| } // namespace |
| |
| // Find all begin access operations in this function. Map each access to |
| // AccessInfo, which includes its identified memory location, identifying |
| // index, and analysis result flags. |
| // |
| // TODO: begin_unpaired_access is not tracked. Even though begin_unpaired_access |
| // isn't explicitly paired, it may be possible after devirtualization and |
| // inlining to find all uses of the scratch buffer. However, this doesn't |
| // currently happen in practice (rdar://40033735). |
| void AccessFolding::identifyBeginAccesses() { |
| for (auto &BB : *F) { |
| for (auto &I : BB) { |
| auto *beginAccess = dyn_cast<BeginAccessInst>(&I); |
| if (!beginAccess) |
| continue; |
| |
| // The accessed base is expected to be valid for begin_access, but for |
| // now, since this optimization runs at the end of the pipeline, we |
| // gracefully handle unrecognized source address patterns, which show up |
| // here as an invalid `storage` value. |
| const AccessedStorage &storage = |
| findAccessedStorageNonNested(beginAccess->getSource()); |
| if (!storage) |
| continue; |
| |
| auto iterAndSuccess = beginAccessMap.try_emplace( |
| beginAccess, static_cast<const AccessInfo &>(storage)); |
| (void)iterAndSuccess; |
| assert(iterAndSuccess.second); |
| |
| // Add a pass-specific access index to the mapped storage object. |
| AccessInfo &info = iterAndSuccess.first->second; |
| info.setAccessIndex(beginAccessMap.size()-1); |
| assert(!info.seenNestedConflict()); |
| } |
| } |
| } |
| |
| /// When a conflict is detected, flag the access so it can't be folded, and |
| /// remove its index from the current access set so we stop checking for |
| /// conflicts. Erasing from SparseAccessSet does not invalidate any iterators. |
| static void recordConflict(AccessInfo &info, SparseAccessSet &accessSet) { |
| info.setSeenNestedConflict(); |
| accessSet.erase(info.getAccessIndex()); |
| } |
| |
| // Given an "inner" access, check for potential conflicts with any outer access. |
| // Allow these overlapping accesses: |
| // - read/read |
| // - different bases, both valid, and at least one is local |
| // |
| // Remove any outer access that may conflict from the accessSet. |
| // and flag the conflict in beginAccessMap. |
| // |
| // Record the inner access in the accessSet. |
| // |
| void AccessFolding::visitBeginAccess(BeginAccessInst *innerBeginAccess, |
| SparseAccessSet &accessSet) { |
| if (innerBeginAccess->getEnforcement() != SILAccessEnforcement::Dynamic) |
| return; |
| |
| const AccessInfo &innerAccess = getAccessInfo(innerBeginAccess); |
| SILAccessKind innerAccessKind = innerBeginAccess->getAccessKind(); |
| |
| SparseAccessSet::Iterator accessIter(accessSet, *this); |
| while (BeginAccessInst *outerBeginAccess = accessIter.next()) { |
| // If both are reads, keep the mapped access. |
| if (!accessKindMayConflict(innerAccessKind, |
| outerBeginAccess->getAccessKind())) { |
| continue; |
| } |
| AccessInfo &outerAccess = getAccessInfo(outerBeginAccess); |
| |
| // If there is no potential conflict, leave the outer access mapped. |
| if (!outerAccess.isDistinctFrom(innerAccess)) |
| continue; |
| |
| DEBUG(innerAccess.dump(); llvm::dbgs() << " may conflict with:\n"; |
| outerAccess.dump()); |
| |
| recordConflict(outerAccess, accessSet); |
| } |
| DEBUG(llvm::dbgs() << "Recording access: " << *innerBeginAccess; |
| llvm::dbgs() << " at: "; innerAccess.dump()); |
| |
| // Record the current access in the map. It can potentially be folded |
| // regardless of whether it may conflict with an outer access. |
| bool inserted = |
| accessSet.insert(innerBeginAccess, innerAccess.getAccessIndex()); |
| (void)inserted; |
| assert(inserted && "the same begin_access cannot be seen twice."); |
| } |
| |
| void AccessFolding::visitEndAccess(EndAccessInst *endAccess, |
| SparseAccessSet &accessSet) { |
| auto *beginAccess = endAccess->getBeginAccess(); |
| if (beginAccess->getEnforcement() != SILAccessEnforcement::Dynamic) |
| return; |
| |
| unsigned index = getAccessIndex(beginAccess); |
| DEBUG(if (accessSet.contains(index)) llvm::dbgs() |
| << "No conflict on one path from " << *beginAccess << " to " |
| << *endAccess); |
| |
| // Erase this access from the sparse set. We only want to detect conflicts |
| // within the access scope. |
| accessSet.erase(index); |
| } |
| |
| void AccessFolding::visitFullApply(FullApplySite fullApply, |
| SparseAccessSet &accessSet) { |
| FunctionAccessedStorage callSiteAccesses; |
| ASA->getCallSiteEffects(callSiteAccesses, fullApply); |
| |
| SparseAccessSet::Iterator accessIter(accessSet, *this); |
| while (BeginAccessInst *outerBeginAccess = accessIter.next()) { |
| |
| // If there is no potential conflict, leave the outer access mapped. |
| SILAccessKind accessKind = outerBeginAccess->getAccessKind(); |
| AccessInfo &outerAccess = getAccessInfo(outerBeginAccess); |
| if (!callSiteAccesses.mayConflictWith(accessKind, outerAccess)) |
| continue; |
| |
| DEBUG(llvm::dbgs() << *fullApply.getInstruction() << " call site access: "; |
| callSiteAccesses.dump(); llvm::dbgs() << " may conflict with:\n"; |
| outerAccess.dump()); |
| |
| recordConflict(outerAccess, accessSet); |
| } |
| } |
| |
| // Merge all predecessor accesses into the local acces set. Only propagate |
| // accesses that are still present in all predecessors. The absence of a begin |
| // access from a visited predecessor indicates the presence of a conflict. A |
| // block has been visited if it has a map entry in blockOutAccess. |
| void AccessFolding::mergePredAccesses(SILBasicBlock *succBB, |
| SparseAccessSet &mergedAccesses) { |
| for (SILBasicBlock *predBB : succBB->getPredecessorBlocks()) { |
| auto mapI = blockOutAccess.find(predBB); |
| if (mapI == blockOutAccess.end()) |
| continue; |
| |
| const DenseAccessSet &predSet = mapI->second; |
| mergedAccesses.merge(SparseAccessSet(predSet, *this)); |
| } |
| } |
| |
| // Compute access reachability within the given block. |
| void AccessFolding::visitBlock(SILBasicBlock *BB) { |
| // Sparse set for tracking accesses with an individual block. |
| SparseAccessSet accessSet(*this); |
| mergePredAccesses(BB, accessSet); |
| |
| for (auto &I : *BB) { |
| if (auto *BAI = dyn_cast<BeginAccessInst>(&I)) { |
| visitBeginAccess(BAI, accessSet); |
| continue; |
| } |
| if (auto *EAI = dyn_cast<EndAccessInst>(&I)) { |
| visitEndAccess(EAI, accessSet); |
| continue; |
| } |
| if (auto fullApply = FullApplySite::isa(&I)) { |
| visitFullApply(fullApply, accessSet); |
| } |
| } |
| if (BB->getTerminator()->isFunctionExiting()) |
| assert(accessSet.isEquivalent(*this) && "no postdominating end_access"); |
| |
| DEBUG(if (!accessSet.isEmpty(*this)) { |
| llvm::dbgs() << "Initializing accesses out of bb" << BB->getDebugID() |
| << "\n"; |
| accessSet.dump(*this); |
| }); |
| |
| // Initialize blockOutAccess for this block with the current access set. |
| accessSet.copyInto(blockOutAccess[BB], *this); |
| } |
| |
| // Data-flow analysis is now complete. Any begin_access remaining in |
| // nonNestedBeginAccessInstructions can be marked "non-nested". |
| // |
| // Note: If we later support marking begin_unpaired_access [no_nested_conflict], |
| // then we also need to remove any corresponding end_unpaired_access. That can |
| // be done either by recording the end_unpaired_access instructions during |
| // analysis and deleting them here in the same order, or sorting them here by |
| // their begin_unpaired_access index. |
| void AccessFolding::foldNonNestedAccesses() { |
| for (auto &beginAccessAndInfo : beginAccessMap) { |
| BeginAccessInst *beginAccess = beginAccessAndInfo.first; |
| AccessInfo &info = beginAccessAndInfo.second; |
| if (info.seenNestedConflict()) |
| continue; |
| |
| // Optimize this begin_access by setting [no_nested_conflict]. |
| beginAccess->setNoNestedConflict(true); |
| } |
| } |
| |
| // Top-level driver for AccessFolding forward data flow optimization. |
| void AccessFolding::perform() { |
| // Populate beginAccessMap. |
| identifyBeginAccesses(); |
| |
| // Perform data flow and set the conflict flags on AccessInfo. |
| for (auto *BB : PO->getReversePostOrder()) |
| visitBlock(BB); |
| |
| // Fold any accesses that don't have nested conflicts. |
| foldNonNestedAccesses(); |
| } |
| |
| namespace { |
| struct AccessEnforcementOpts : public SILFunctionTransform { |
| void run() override { |
| SILFunction *F = getFunction(); |
| if (F->empty()) |
| return; |
| |
| // Perform access folding. May mark begin_access with no_nested_conflict and |
| // may removed end_unpaired_access. |
| PostOrderFunctionInfo *PO = getAnalysis<PostOrderAnalysis>()->get(F); |
| AccessedStorageAnalysis *ASA = getAnalysis<AccessedStorageAnalysis>(); |
| AccessFolding folding(F, PO, ASA); |
| folding.perform(); |
| } |
| }; |
| } |
| |
| SILTransform *swift::createAccessEnforcementOpts() { |
| return new AccessEnforcementOpts(); |
| } |