Skip to content

Commit ec5dd42

Browse files
committed
Optimizer: reimplement DeadStoreElimination in swift
The old C++ pass didn't catch a few cases. Also: * The new pass is significantly simpler: it doesn't perform dataflow for _all_ memory locations at once using bitfields, but handles each store separately. (In both implementations there is a complexity limit in place to avoid quadratic complexity) * The new pass works with OSSA
1 parent 0dbd5c6 commit ec5dd42

File tree

9 files changed

+323
-1316
lines changed

9 files changed

+323
-1316
lines changed

SwiftCompilerSources/Sources/Optimizer/FunctionPasses/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ swift_compiler_sources(Optimizer
1111
CleanupDebugSteps.swift
1212
ComputeEscapeEffects.swift
1313
ComputeSideEffects.swift
14+
DeadStoreElimination.swift
1415
InitializeStaticGlobals.swift
1516
ObjectOutliner.swift
1617
ObjCBridgingOptimization.swift
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
//===--- DeadStoreElimination.swift ----------------------------------------==//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SIL
14+
15+
/// Eliminates dead store instructions.
16+
///
17+
/// A store is dead if after the store has occurred:
18+
///
19+
/// 1. The value in memory is not read until the memory object is deallocated:
20+
///
21+
/// %1 = alloc_stack
22+
/// ...
23+
/// store %2 to %1
24+
/// ... // no reads from %1
25+
/// dealloc_stack %1
26+
///
27+
/// 2. The value in memory is overwritten by another store before any potential read:
28+
///
29+
/// store %2 to %1
30+
/// ... // no reads from %1
31+
/// store %3 to %1
32+
///
33+
/// In case of a partial dead store, the store is split so that some of the new
34+
/// individual stores can be eliminated in the next round of the optimization:
35+
///
36+
/// store %2 to %1 // partially dead
37+
/// ... // no reads from %1
38+
/// %3 = struct_element_addr %1, #field1
39+
/// store %7 to %3
40+
/// ->
41+
/// %3 = struct_extract %2, #field1
42+
/// %4 = struct_element_addr %1, #field1
43+
/// store %3 to %4 // this store is dead now
44+
/// %5 = struct_extract %2, #field2
45+
/// %6 = struct_element_addr %1, #field2
46+
/// store %5 to %6
47+
/// ... // no reads from %1
48+
/// store %7 to %3
49+
///
50+
let deadStoreElimination = FunctionPass(name: "dead-store-elimination") {
51+
(function: Function, context: FunctionPassContext) in
52+
53+
for block in function.blocks {
54+
55+
// We cannot use for-in iteration here because if the store is split, the new
56+
// individual stores are inserted right afterwards and they would be ignored by a for-in iteration.
57+
var inst = block.instructions.first
58+
while let i = inst {
59+
if let store = i as? StoreInst {
60+
if !context.continueWithNextSubpassRun(for: store) {
61+
return
62+
}
63+
tryEliminate(store: store, context)
64+
}
65+
inst = i.next
66+
}
67+
}
68+
}
69+
70+
private func tryEliminate(store: StoreInst, _ context: FunctionPassContext) {
71+
if !store.hasValidOwnershipForDeadStoreElimination {
72+
return
73+
}
74+
75+
switch store.isDead(context) {
76+
case .alive:
77+
break
78+
case .dead:
79+
context.erase(instruction: store)
80+
case .maybePartiallyDead(let subPath):
81+
// Check if the a partial store would really be dead to avoid unnecessary splitting.
82+
switch store.isDead(at: subPath, context) {
83+
case .alive, .maybePartiallyDead:
84+
break
85+
case .dead:
86+
// The new individual stores are inserted right after the current store and
87+
// will be optimized in the following loop iterations.
88+
store.trySplit(context)
89+
}
90+
}
91+
}
92+
93+
private extension StoreInst {
94+
95+
enum DataflowResult {
96+
case alive
97+
case dead
98+
case maybePartiallyDead(AccessPath)
99+
100+
init(aliveWith subPath: AccessPath?) {
101+
if let subPath = subPath {
102+
self = .maybePartiallyDead(subPath)
103+
} else {
104+
self = .alive
105+
}
106+
}
107+
}
108+
109+
func isDead( _ context: FunctionPassContext) -> DataflowResult {
110+
return isDead(at: destination.accessPath, context)
111+
}
112+
113+
func isDead(at accessPath: AccessPath, _ context: FunctionPassContext) -> DataflowResult {
114+
var worklist = InstructionWorklist(context)
115+
defer { worklist.deinitialize() }
116+
117+
worklist.pushIfNotVisited(self.next!)
118+
119+
let storageDefBlock = accessPath.base.reference?.referenceRoot.parentBlock
120+
var scanner = InstructionScanner(storePath: accessPath, storeAddress: self.destination, context.aliasAnalysis)
121+
122+
while let startInstInBlock = worklist.pop() {
123+
let block = startInstInBlock.parentBlock
124+
switch scanner.scan(instructions: InstructionList(first: startInstInBlock)) {
125+
case .transparent:
126+
// Abort if we find the storage definition of the access in case of a loop, e.g.
127+
//
128+
// bb1:
129+
// %storage_root = apply
130+
// %2 = ref_element_addr %storage_root
131+
// store %3 to %2
132+
// cond_br %c, bb1, bb2
133+
//
134+
// The storage root is different in each loop iteration. Therefore the store of a
135+
// successive loop iteration does not overwrite the store of the previous iteration.
136+
if let storageDefBlock = storageDefBlock,
137+
block.successors.contains(storageDefBlock) {
138+
return DataflowResult(aliveWith: scanner.potentiallyDeadSubpath)
139+
}
140+
worklist.pushIfNotVisited(contentsOf: block.successors.lazy.map { $0.instructions.first! })
141+
case .dead:
142+
break
143+
case .alive:
144+
return DataflowResult(aliveWith: scanner.potentiallyDeadSubpath)
145+
}
146+
}
147+
return .dead
148+
}
149+
150+
func trySplit(_ context: FunctionPassContext) {
151+
let type = source.type
152+
if type.isStruct || type.isTuple {
153+
let builder = Builder(after: self, context)
154+
for idx in 0..<type.getNominalFields(in: parentFunction).count {
155+
let srcField: Value
156+
let destFieldAddr: Value
157+
if type.isStruct {
158+
srcField = builder.createStructExtract(struct: source, fieldIndex: idx)
159+
destFieldAddr = builder.createStructElementAddr(structAddress: destination, fieldIndex: idx)
160+
} else {
161+
srcField = builder.createTupleExtract(tuple: source, elementIndex: idx)
162+
destFieldAddr = builder.createTupleElementAddr(tupleAddress: destination, elementIndex: idx)
163+
}
164+
builder.createStore(source: srcField, destination: destFieldAddr, ownership: destinationOwnership)
165+
}
166+
context.erase(instruction: self)
167+
}
168+
}
169+
170+
var hasValidOwnershipForDeadStoreElimination: Bool {
171+
switch destinationOwnership {
172+
case .unqualified, .trivial:
173+
return true
174+
case .initialize, .assign:
175+
// In OSSA, non-trivial values cannot be dead-store eliminated because that could shrink
176+
// the lifetime of the original stored value (because it's not kept in memory anymore).
177+
return false
178+
}
179+
}
180+
}
181+
182+
private struct InstructionScanner {
183+
let storePath: AccessPath
184+
let storeAddress: Value
185+
let aliasAnalysis: AliasAnalysis
186+
187+
var potentiallyDeadSubpath: AccessPath? = nil
188+
189+
// Avoid quadratic complexity by limiting the number of visited instructions for each store.
190+
// The limit of 1000 instructions is not reached by far in "real-world" functions.
191+
private var budget = 1000
192+
193+
init(storePath: AccessPath, storeAddress: Value, _ aliasAnalysis: AliasAnalysis) {
194+
self.storePath = storePath
195+
self.storeAddress = storeAddress
196+
self.aliasAnalysis = aliasAnalysis
197+
}
198+
199+
enum Result {
200+
case alive
201+
case dead
202+
case transparent
203+
}
204+
205+
mutating func scan(instructions: InstructionList) -> Result {
206+
for inst in instructions {
207+
switch inst {
208+
case let successiveStore as StoreInst:
209+
let successivePath = successiveStore.destination.accessPath
210+
if successivePath.isEqualOrOverlaps(storePath) {
211+
return .dead
212+
}
213+
if storePath.isEqualOrOverlaps(successivePath),
214+
potentiallyDeadSubpath == nil {
215+
// Storing to a sub-field of the original store doesn't make the original store dead.
216+
// But when we split the original store, then one of the new individual stores might be
217+
// overwritten by this store.
218+
potentiallyDeadSubpath = successivePath
219+
}
220+
case is DeallocRefInst, is DeallocStackRefInst, is DeallocBoxInst:
221+
if (inst as! Deallocation).isDeallocation(of: storePath.base) {
222+
return .dead
223+
}
224+
case let ds as DeallocStackInst:
225+
if ds.isStackDeallocation(of: storePath.base) {
226+
return .dead
227+
}
228+
case is FixLifetimeInst, is EndAccessInst:
229+
break
230+
case let term as TermInst:
231+
if term.isFunctionExiting {
232+
return .alive
233+
}
234+
fallthrough
235+
default:
236+
budget -= 1
237+
if budget == 0 {
238+
return .alive
239+
}
240+
if inst.mayRead(fromAddress: storeAddress, aliasAnalysis) {
241+
return .alive
242+
}
243+
}
244+
}
245+
return .transparent
246+
}
247+
}
248+
249+
private extension Deallocation {
250+
func isDeallocation(of base: AccessBase) -> Bool {
251+
if let accessReference = base.reference,
252+
accessReference.referenceRoot == self.allocatedValue.referenceRoot {
253+
return true
254+
}
255+
return false
256+
}
257+
}
258+
259+
private extension DeallocStackInst {
260+
func isStackDeallocation(of base: AccessBase) -> Bool {
261+
if case .stack(let allocStack) = base, allocstack == allocStack {
262+
return true
263+
}
264+
return false
265+
}
266+
}

SwiftCompilerSources/Sources/Optimizer/PassManager/PassRegistration.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ private func registerSwiftPasses() {
8282
registerPass(cleanupDebugStepsPass, { cleanupDebugStepsPass.run($0) })
8383
registerPass(namedReturnValueOptimization, { namedReturnValueOptimization.run($0) })
8484
registerPass(stripObjectHeadersPass, { stripObjectHeadersPass.run($0) })
85+
registerPass(deadStoreElimination, { deadStoreElimination.run($0) })
8586

8687
// Instruction passes
8788
registerForSILCombine(BeginCOWMutationInst.self, { run(BeginCOWMutationInst.self, $0) })

include/swift/SILOptimizer/PassManager/Passes.def

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ PASS(EarlyRedundantLoadElimination, "early-redundant-load-elim",
243243
"Early Redundant Load Elimination")
244244
PASS(RedundantLoadElimination, "redundant-load-elim",
245245
"Redundant Load Elimination")
246-
PASS(DeadStoreElimination, "dead-store-elim",
246+
SWIFT_FUNCTION_PASS(DeadStoreElimination, "dead-store-elimination",
247247
"Dead Store Elimination")
248248
PASS(GenericSpecializer, "generic-specializer",
249249
"Generic Function Specialization on Static Types")

lib/SILOptimizer/Transforms/CMakeLists.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ target_sources(swiftSILOptimizer PRIVATE
1414
CopyPropagation.cpp
1515
DeadCodeElimination.cpp
1616
DeadObjectElimination.cpp
17-
DeadStoreElimination.cpp
1817
DestroyHoisting.cpp
1918
Devirtualizer.cpp
2019
DifferentiabilityWitnessDevirtualizer.cpp

0 commit comments

Comments
 (0)