Skip to content

Commit 5b42469

Browse files
committed
Reduce compile time for large strongly connected call graphs.
Improves SwiftSyntax release build speed by 4x. Limit the size of the sets tracked by inter procedural AccessedStorageAnalysis. There is a lot of leeway in this limit since "normal" code doesn't come close to hitting it and SwiftSyntax compile time isn't noticeably affected until 10x this limit. This change also avoids reanalyzing function bodies once results have bottomed out and avoids copying sets in the common case. Fixes <rdar://problem/46905624> Release build time regression, a lot of time spent on AccessEnforcementOpts::run(). This is just a band aid. The fundamental problem is really: <rdar://problem/47195282> Recomputing BottomUpIPAnalysis takes most of the SwiftSyntax compile time. SwiftSyntax compile time is still at least an order of magnitude longer than it should be.
1 parent be33d17 commit 5b42469

File tree

3 files changed

+48
-22
lines changed

3 files changed

+48
-22
lines changed

include/swift/SILOptimizer/Analysis/AccessedStorageAnalysis.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
// used by AccessEnforcementOpts to locally fold access scopes and remove
1616
// dynamic checks based on whole module analysis.
1717
//
18+
// This analysis may return conservative results by setting
19+
// FunctionAccessedStorage.unidentifiedAccess. This does not imply that all
20+
// accesses within the function have Unidentified AccessedStorage.
21+
//
1822
// Note: This interprocedural analysis can be easily augmented to simultaneously
1923
// compute FunctionSideEffects, without using a separate analysis, by adding
2024
// FunctionSideEffects as a member of FunctionAccessedStorage. However, passes
@@ -125,7 +129,10 @@ namespace swift {
125129
/// additional StorageAccessInfo bits are recorded as results of this analysis.
126130
///
127131
/// Any unidentified accesses are summarized as a single unidentifiedAccess
128-
/// property.
132+
/// property. This property may also be used to conservatively summarize
133+
/// results, either because the call graph is unknown or the access sets are too
134+
/// large. It does not imply that all accesses have Unidentified
135+
/// AccessedStorage, which is never allowed for class or global access.
129136
class FunctionAccessedStorage {
130137
using AccessedStorageSet = llvm::SmallDenseSet<StorageAccessInfo, 8>;
131138

@@ -164,6 +171,9 @@ class FunctionAccessedStorage {
164171
unidentifiedAccess = None;
165172
}
166173

174+
/// Return true if these effects are fully conservative.
175+
bool hasWorstEffects() { return unidentifiedAccess == SILAccessKind::Modify; }
176+
167177
/// Sets the most conservative effects, if we don't know anything about the
168178
/// function.
169179
void setWorstEffects() {

lib/SILOptimizer/Analysis/AccessedStorageAnalysis.cpp

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -151,29 +151,43 @@ bool FunctionAccessedStorage::updateUnidentifiedAccess(
151151
// substitution will be performed if possible. However, there's no guarantee
152152
// that the merged access values will belong to this function.
153153
//
154-
// Note that we may have `this` == `other` for self-recursion. We still need to
155-
// propagate and merge in that case in case arguments are recursively dependent.
154+
// Return true if these results changed, requiring further propagation through
155+
// the call graph.
156156
bool FunctionAccessedStorage::mergeAccesses(
157157
const FunctionAccessedStorage &other,
158158
std::function<StorageAccessInfo(const StorageAccessInfo &)>
159159
transformStorage) {
160160

161-
// Insertion in DenseMap invalidates the iterator in the rare case of
162-
// self-recursion (`this` == `other`) that passes accessed storage though an
163-
// argument. Rather than complicate the code, make a temporary copy of the
164-
// AccessedStorage.
165-
//
166-
// Also note that the storageAccessIndex from otherStorage is relative to its
167-
// original context and should not be copied into this context.
168-
SmallVector<StorageAccessInfo, 8> otherStorageAccesses;
169-
otherStorageAccesses.reserve(other.storageAccessSet.size());
170-
otherStorageAccesses.append(other.storageAccessSet.begin(),
171-
other.storageAccessSet.end());
161+
// The cost of BottomUpIPAnalysis can be quadratic for large recursive call
162+
// graphs. That cost is multiplied by the size of storageAccessSet. Slowdowns
163+
// can occur ~1000 elements. 200 is large enough to cover "normal" code,
164+
// while ensuring compile time isn't affected.
165+
if (storageAccessSet.size() > 200) {
166+
llvm::dbgs() << "BIG SET " << storageAccessSet.size() << "\n";
167+
setWorstEffects();
168+
return true;
169+
}
170+
// To save compile time, if this storage already has worst-case effects, avoid
171+
// growing its storageAccessSet.
172+
if (hasWorstEffects())
173+
return false;
172174

175+
// When `this` == `other` (for self-recursion), insertion in DenseMap
176+
// invalidates the iterator. We still need to propagate and merge in that case
177+
// because arguments can be recursively dependent. The alternative would be
178+
// treating all self-recursion conservatively.
179+
const FunctionAccessedStorage *otherFunctionAccesses = &other;
180+
FunctionAccessedStorage functionAccessCopy;
181+
if (this == &other) {
182+
functionAccessCopy = other;
183+
otherFunctionAccesses = &functionAccessCopy;
184+
}
173185
bool changed = false;
174-
for (auto &rawStorageInfo : otherStorageAccesses) {
186+
// Nondeterminstically iterate for the sole purpose of inserting into another
187+
// unordered set.
188+
for (auto &rawStorageInfo : otherFunctionAccesses->storageAccessSet) {
175189
const StorageAccessInfo &otherStorageInfo =
176-
transformStorage(rawStorageInfo);
190+
transformStorage(rawStorageInfo);
177191
// If transformStorage() returns invalid storage object for local storage,
178192
// that should not be merged with the caller.
179193
if (!otherStorageInfo)

lib/SILOptimizer/Transforms/AccessEnforcementWMO.cpp

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
///
1616
/// This maps each access of identified storage onto a disjoint access
1717
/// location. Local accesses (Box and Stack) already have unique AccessedStorage
18-
/// and should be removed by an earlier function pass. This pass handles Class
19-
/// and Global access by partitioning their non-unique AccessedStorage objects
20-
/// into unique DisjointAccessLocations. These disjoint access locations may be
21-
/// accessed across multiple functions, so a module pass is required to identify
22-
/// and optimize them.
18+
/// and should be removed by an earlier function pass if possible. This pass
19+
/// handles Class and Global access by partitioning their non-unique
20+
/// AccessedStorage objects into unique DisjointAccessLocations. These disjoint
21+
/// access locations may be accessed across multiple functions, so a module pass
22+
/// is required to identify and optimize them.
2323
///
2424
/// Class accesses are partitioned by their fully qualified property
2525
/// name. Global accesses are partitioned by the global variable name. Argument
@@ -48,7 +48,9 @@
4848
///
4949
/// Note: This optimization must be aware of all possible access to a Class or
5050
/// Global address. This includes unpaired access instructions and keypath
51-
/// instructions. Ignoring any access pattern would weaken enforcement.
51+
/// instructions. Ignoring any access pattern would weaken enforcement. For
52+
/// example, AccessedStorageAnalysis cannot be used here because that analysis
53+
/// may conservatively summarize some functions.
5254
//===----------------------------------------------------------------------===//
5355

5456
#define DEBUG_TYPE "access-enforcement-wmo"

0 commit comments

Comments
 (0)