Skip to content

Commit 051ffc5

Browse files
authored
Merge pull request #69199 from eeckstein/fix-compile-time
Optimizer: better handling of the complexity budget in redundant-load-elimination and dead-store-elimination
2 parents 22e79c2 + 54d254f commit 051ffc5

File tree

3 files changed

+73
-35
lines changed

3 files changed

+73
-35
lines changed

SwiftCompilerSources/Sources/Optimizer/FunctionPasses/DeadStoreElimination.swift

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ import SIL
5353
let deadStoreElimination = FunctionPass(name: "dead-store-elimination") {
5454
(function: Function, context: FunctionPassContext) in
5555

56+
// Avoid quadratic complexity by limiting the number of visited instructions.
57+
// This limit is sufficient for most "real-world" functions, by far.
58+
var complexityBudget = 10_000
59+
5660
for block in function.blocks {
5761

5862
// We cannot use for-in iteration here because if the store is split, the new
@@ -63,26 +67,26 @@ let deadStoreElimination = FunctionPass(name: "dead-store-elimination") {
6367
if !context.continueWithNextSubpassRun(for: store) {
6468
return
6569
}
66-
tryEliminate(store: store, context)
70+
tryEliminate(store: store, complexityBudget: &complexityBudget, context)
6771
}
6872
inst = i.next
6973
}
7074
}
7175
}
7276

73-
private func tryEliminate(store: StoreInst, _ context: FunctionPassContext) {
77+
private func tryEliminate(store: StoreInst, complexityBudget: inout Int, _ context: FunctionPassContext) {
7478
if !store.hasValidOwnershipForDeadStoreElimination {
7579
return
7680
}
7781

78-
switch store.isDead(context) {
82+
switch store.isDead(complexityBudget: &complexityBudget, context) {
7983
case .alive:
8084
break
8185
case .dead:
8286
context.erase(instruction: store)
8387
case .maybePartiallyDead(let subPath):
8488
// Check if the a partial store would really be dead to avoid unnecessary splitting.
85-
switch store.isDead(at: subPath, context) {
89+
switch store.isDead(at: subPath, complexityBudget: &complexityBudget, context) {
8690
case .alive, .maybePartiallyDead:
8791
break
8892
case .dead:
@@ -109,15 +113,15 @@ private extension StoreInst {
109113
}
110114
}
111115

112-
func isDead( _ context: FunctionPassContext) -> DataflowResult {
113-
return isDead(at: destination.accessPath, context)
116+
func isDead(complexityBudget: inout Int, _ context: FunctionPassContext) -> DataflowResult {
117+
return isDead(at: destination.accessPath, complexityBudget: &complexityBudget, context)
114118
}
115119

116-
func isDead(at accessPath: AccessPath, _ context: FunctionPassContext) -> DataflowResult {
120+
func isDead(at accessPath: AccessPath, complexityBudget: inout Int, _ context: FunctionPassContext) -> DataflowResult {
117121
var scanner = InstructionScanner(storePath: accessPath, storeAddress: self.destination, context.aliasAnalysis)
118122
let storageDefBlock = accessPath.base.reference?.referenceRoot.parentBlock
119123

120-
switch scanner.scan(instructions: InstructionList(first: self.next)) {
124+
switch scanner.scan(instructions: InstructionList(first: self.next), complexityBudget: &complexityBudget) {
121125
case .dead:
122126
return .dead
123127

@@ -145,7 +149,7 @@ private extension StoreInst {
145149
if let storageDefBlock = storageDefBlock, block == storageDefBlock {
146150
return DataflowResult(aliveWith: scanner.potentiallyDeadSubpath)
147151
}
148-
switch scanner.scan(instructions: block.instructions) {
152+
switch scanner.scan(instructions: block.instructions, complexityBudget: &complexityBudget) {
149153
case .transparent:
150154
worklist.pushIfNotVisited(contentsOf: block.successors)
151155
case .dead:
@@ -177,10 +181,6 @@ private struct InstructionScanner {
177181

178182
private(set) var potentiallyDeadSubpath: AccessPath? = nil
179183

180-
// Avoid quadratic complexity by limiting the number of visited instructions for each store.
181-
// The limit of 1000 instructions is not reached by far in "real-world" functions.
182-
private var budget = 1000
183-
184184
init(storePath: AccessPath, storeAddress: Value, _ aliasAnalysis: AliasAnalysis) {
185185
self.storePath = storePath
186186
self.storeAddress = storeAddress
@@ -193,7 +193,7 @@ private struct InstructionScanner {
193193
case transparent
194194
}
195195

196-
mutating func scan(instructions: InstructionList) -> Result {
196+
mutating func scan(instructions: InstructionList, complexityBudget: inout Int) -> Result {
197197
for inst in instructions {
198198
switch inst {
199199
case let successiveStore as StoreInst:
@@ -226,8 +226,8 @@ private struct InstructionScanner {
226226
}
227227
fallthrough
228228
default:
229-
budget -= 1
230-
if budget == 0 {
229+
complexityBudget -= 1
230+
if complexityBudget <= 0 {
231231
return .alive
232232
}
233233
if inst.mayRead(fromAddress: storeAddress, aliasAnalysis) {

SwiftCompilerSources/Sources/Optimizer/FunctionPasses/RedundantLoadElimination.swift

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ let earlyRedundantLoadElimination = FunctionPass(name: "early-redundant-load-eli
7474
}
7575

7676
private func eliminateRedundantLoads(in function: Function, ignoreArrays: Bool, _ context: FunctionPassContext) {
77+
78+
// Avoid quadratic complexity by limiting the number of visited instructions.
79+
// This limit is sufficient for most "real-world" functions, by far.
80+
var complexityBudget = 50_000
81+
7782
for block in function.blocks.reversed() {
7883

7984
// We cannot use for-in iteration here because if the load is split, the new
@@ -89,21 +94,21 @@ private func eliminateRedundantLoads(in function: Function, ignoreArrays: Bool,
8994
if ignoreArrays && load.type.isNominal && load.type.nominal == context.swiftArrayDecl {
9095
continue
9196
}
92-
tryEliminate(load: load, context)
97+
tryEliminate(load: load, complexityBudget: &complexityBudget, context)
9398
}
9499
}
95100
}
96101
}
97102

98-
private func tryEliminate(load: LoadInst, _ context: FunctionPassContext) {
99-
switch load.isRedundant(context) {
103+
private func tryEliminate(load: LoadInst, complexityBudget: inout Int, _ context: FunctionPassContext) {
104+
switch load.isRedundant(complexityBudget: &complexityBudget, context) {
100105
case .notRedundant:
101106
break
102107
case .redundant(let availableValues):
103108
replace(load: load, with: availableValues, context)
104109
case .maybePartiallyRedundant(let subPath):
105110
// Check if the a partial load would really be redundant to avoid unnecessary splitting.
106-
switch load.isRedundant(at: subPath, context) {
111+
switch load.isRedundant(at: subPath, complexityBudget: &complexityBudget, context) {
107112
case .notRedundant, .maybePartiallyRedundant:
108113
break
109114
case .redundant:
@@ -130,25 +135,29 @@ private extension LoadInst {
130135
}
131136
}
132137

133-
func isRedundant(_ context: FunctionPassContext) -> DataflowResult {
134-
return isRedundant(at: address.accessPath, context)
138+
func isRedundant(complexityBudget: inout Int, _ context: FunctionPassContext) -> DataflowResult {
139+
return isRedundant(at: address.accessPath, complexityBudget: &complexityBudget, context)
135140
}
136141

137-
func isRedundant(at accessPath: AccessPath, _ context: FunctionPassContext) -> DataflowResult {
142+
func isRedundant(at accessPath: AccessPath, complexityBudget: inout Int, _ context: FunctionPassContext) -> DataflowResult {
138143
var scanner = InstructionScanner(load: self, accessPath: accessPath, context.aliasAnalysis)
139144

140-
switch scanner.scan(instructions: ReverseInstructionList(first: self.previous), in: parentBlock) {
145+
switch scanner.scan(instructions: ReverseInstructionList(first: self.previous),
146+
in: parentBlock,
147+
complexityBudget: &complexityBudget)
148+
{
141149
case .overwritten:
142150
return DataflowResult(notRedundantWith: scanner.potentiallyRedundantSubpath)
143151
case .available:
144152
return .redundant(scanner.availableValues)
145153
case .transparent:
146-
return self.isRedundantInPredecessorBlocks(scanner: &scanner, context)
154+
return self.isRedundantInPredecessorBlocks(scanner: &scanner, complexityBudget: &complexityBudget, context)
147155
}
148156
}
149157

150158
private func isRedundantInPredecessorBlocks(
151159
scanner: inout InstructionScanner,
160+
complexityBudget: inout Int,
152161
_ context: FunctionPassContext
153162
) -> DataflowResult {
154163

@@ -157,7 +166,10 @@ private extension LoadInst {
157166
liferange.pushPredecessors(of: self.parentBlock)
158167

159168
while let block = liferange.pop() {
160-
switch scanner.scan(instructions: block.instructions.reversed(), in: block) {
169+
switch scanner.scan(instructions: block.instructions.reversed(),
170+
in: block,
171+
complexityBudget: &complexityBudget)
172+
{
161173
case .overwritten:
162174
return DataflowResult(notRedundantWith: scanner.potentiallyRedundantSubpath)
163175
case .available:
@@ -402,10 +414,6 @@ private struct InstructionScanner {
402414
private(set) var potentiallyRedundantSubpath: AccessPath? = nil
403415
private(set) var availableValues = Array<AvailableValue>()
404416

405-
// Avoid quadratic complexity by limiting the number of visited instructions for each store.
406-
// The limit of 1000 instructions is not reached by far in "real-world" functions.
407-
private var budget = 1000
408-
409417
init(load: LoadInst, accessPath: AccessPath, _ aliasAnalysis: AliasAnalysis) {
410418
self.load = load
411419
self.accessPath = accessPath
@@ -419,8 +427,16 @@ private struct InstructionScanner {
419427
case transparent
420428
}
421429

422-
mutating func scan(instructions: ReverseInstructionList, in block: BasicBlock) -> ScanResult {
430+
mutating func scan(instructions: ReverseInstructionList,
431+
in block: BasicBlock,
432+
complexityBudget: inout Int) -> ScanResult
433+
{
423434
for inst in instructions {
435+
complexityBudget -= 1
436+
if complexityBudget <= 0 {
437+
return .overwritten
438+
}
439+
424440
switch visit(instruction: inst) {
425441
case .available: return .available
426442
case .overwritten: return .overwritten
@@ -490,10 +506,6 @@ private struct InstructionScanner {
490506
default:
491507
break
492508
}
493-
budget -= 1
494-
if budget == 0 {
495-
return .overwritten
496-
}
497509
if load.loadOwnership == .take {
498510
// In case of `take`, don't allow reading instructions in the liferange.
499511
// Otherwise we cannot shrink the memory liferange afterwards.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
2+
// The compiler should finish in about 5 seconds. To give some slack,
3+
// specify a timeout of 60 seconds
4+
// If the compiler needs more than 60 seconds, there is probably a real problem.
5+
// So please don't just increase the timeout in case this test fails.
6+
7+
// RUN: %{python} %S/../../test/Inputs/timeout.py 60 %target-swift-frontend -O -parse-as-library -sil-verify-none -emit-sil %s | %FileCheck %s
8+
9+
// REQUIRES: swift_stdlib_no_asserts,optimized_stdlib
10+
// REQUIRES: CPU=arm64 || CPU=x86_64 || CPU=aarch64
11+
// REQUIRES: OS=macosx
12+
13+
import Darwin
14+
15+
// Check that redundant-load-elimination and dead-store-elimination don't take
16+
// extermely long when optimizing statfs, which contains a 1023-element tuple.
17+
18+
// CHECK-LABEL: test_rle_dse_compile_time
19+
public func test_rle_dse_compile_time(_ s: statfs) {
20+
withUnsafePointer(to: s.f_mntonname) {
21+
$0.withMemoryRebound(to: UInt8.self, capacity: Int(MAXPATHLEN)) { (str) in
22+
print(str)
23+
}
24+
}
25+
}
26+

0 commit comments

Comments
 (0)