Skip to content

Commit 9cb83c3

Browse files
committed
DeadStoreElimination: some refactoring and improvements
Addresses review feedback of swiftlang#67122
1 parent efcd90a commit 9cb83c3

File tree

3 files changed

+156
-76
lines changed

3 files changed

+156
-76
lines changed

SwiftCompilerSources/Sources/Optimizer/FunctionPasses/DeadStoreElimination.swift

Lines changed: 46 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import SIL
1414

1515
/// Eliminates dead store instructions.
1616
///
17-
/// A store is dead if after the store has occurred:
17+
/// A store is dead if, after the store has occurred:
1818
///
1919
/// 1. The value in memory is not read until the memory object is deallocated:
2020
///
@@ -47,6 +47,9 @@ import SIL
4747
/// ... // no reads from %1
4848
/// store %7 to %3
4949
///
50+
/// The algorithm is a data flow analysis which starts at the original store and searches
51+
/// for successive stores by following the control flow in forward direction.
52+
///
5053
let deadStoreElimination = FunctionPass(name: "dead-store-elimination") {
5154
(function: Function, context: FunctionPassContext) in
5255

@@ -85,7 +88,7 @@ private func tryEliminate(store: StoreInst, _ context: FunctionPassContext) {
8588
case .dead:
8689
// The new individual stores are inserted right after the current store and
8790
// will be optimized in the following loop iterations.
88-
store.trySplit(context)
91+
store.split(context)
8992
}
9093
}
9194
}
@@ -111,18 +114,24 @@ private extension StoreInst {
111114
}
112115

113116
func isDead(at accessPath: AccessPath, _ context: FunctionPassContext) -> DataflowResult {
114-
var worklist = InstructionWorklist(context)
115-
defer { worklist.deinitialize() }
117+
var scanner = InstructionScanner(storePath: accessPath, storeAddress: self.destination, context.aliasAnalysis)
118+
let storageDefBlock = accessPath.base.reference?.referenceRoot.parentBlock
116119

117-
worklist.pushIfNotVisited(self.next!)
120+
switch scanner.scan(instructions: InstructionList(first: self.next)) {
121+
case .dead:
122+
return .dead
118123

119-
let storageDefBlock = accessPath.base.reference?.referenceRoot.parentBlock
120-
var scanner = InstructionScanner(storePath: accessPath, storeAddress: self.destination, context.aliasAnalysis)
124+
case .alive:
125+
return DataflowResult(aliveWith: scanner.potentiallyDeadSubpath)
126+
127+
case .transparent:
128+
// Continue with iterative data flow analysis starting at the block's successors.
129+
var worklist = BasicBlockWorklist(context)
130+
defer { worklist.deinitialize() }
131+
worklist.pushIfNotVisited(contentsOf: self.parentBlock.successors)
132+
133+
while let block = worklist.pop() {
121134

122-
while let startInstInBlock = worklist.pop() {
123-
let block = startInstInBlock.parentBlock
124-
switch scanner.scan(instructions: InstructionList(first: startInstInBlock)) {
125-
case .transparent:
126135
// Abort if we find the storage definition of the access in case of a loop, e.g.
127136
//
128137
// bb1:
@@ -133,39 +142,41 @@ private extension StoreInst {
133142
//
134143
// The storage root is different in each loop iteration. Therefore the store of a
135144
// successive loop iteration does not overwrite the store of the previous iteration.
136-
if let storageDefBlock = storageDefBlock,
137-
block.successors.contains(storageDefBlock) {
145+
if let storageDefBlock = storageDefBlock, block == storageDefBlock {
146+
return DataflowResult(aliveWith: scanner.potentiallyDeadSubpath)
147+
}
148+
switch scanner.scan(instructions: block.instructions) {
149+
case .transparent:
150+
worklist.pushIfNotVisited(contentsOf: block.successors)
151+
case .dead:
152+
break
153+
case .alive:
138154
return DataflowResult(aliveWith: scanner.potentiallyDeadSubpath)
139155
}
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)
145156
}
157+
return .dead
146158
}
147-
return .dead
148159
}
149160

150-
func trySplit(_ context: FunctionPassContext) {
161+
func split(_ context: FunctionPassContext) {
162+
let builder = Builder(after: self, context)
151163
let type = source.type
152164
if type.isStruct {
153-
let builder = Builder(after: self, context)
154165
for idx in 0..<type.getNominalFields(in: parentFunction).count {
155166
let srcField = builder.createStructExtract(struct: source, fieldIndex: idx)
156167
let destFieldAddr = builder.createStructElementAddr(structAddress: destination, fieldIndex: idx)
157168
builder.createStore(source: srcField, destination: destFieldAddr, ownership: storeOwnership)
158169
}
159-
context.erase(instruction: self)
160170
} else if type.isTuple {
161-
let builder = Builder(after: self, context)
162171
for idx in 0..<type.tupleElements.count {
163172
let srcField = builder.createTupleExtract(tuple: source, elementIndex: idx)
164173
let destFieldAddr = builder.createTupleElementAddr(tupleAddress: destination, elementIndex: idx)
165174
builder.createStore(source: srcField, destination: destFieldAddr, ownership: storeOwnership)
166175
}
167-
context.erase(instruction: self)
176+
} else {
177+
fatalError("a materializable projection path should only contain struct and tuple projections")
168178
}
179+
context.erase(instruction: self)
169180
}
170181

171182
var hasValidOwnershipForDeadStoreElimination: Bool {
@@ -181,11 +192,11 @@ private extension StoreInst {
181192
}
182193

183194
private struct InstructionScanner {
184-
let storePath: AccessPath
185-
let storeAddress: Value
186-
let aliasAnalysis: AliasAnalysis
195+
private let storePath: AccessPath
196+
private let storeAddress: Value
197+
private let aliasAnalysis: AliasAnalysis
187198

188-
var potentiallyDeadSubpath: AccessPath? = nil
199+
private(set) var potentiallyDeadSubpath: AccessPath? = nil
189200

190201
// Avoid quadratic complexity by limiting the number of visited instructions for each store.
191202
// The limit of 1000 instructions is not reached by far in "real-world" functions.
@@ -208,14 +219,16 @@ private struct InstructionScanner {
208219
switch inst {
209220
case let successiveStore as StoreInst:
210221
let successivePath = successiveStore.destination.accessPath
211-
if successivePath.isEqualOrOverlaps(storePath) {
222+
if successivePath.isEqualOrContains(storePath) {
212223
return .dead
213224
}
214-
if storePath.isEqualOrOverlaps(successivePath),
215-
potentiallyDeadSubpath == nil {
225+
if potentiallyDeadSubpath == nil,
226+
storePath.getMaterializableProjection(to: successivePath) != nil {
216227
// Storing to a sub-field of the original store doesn't make the original store dead.
217228
// But when we split the original store, then one of the new individual stores might be
218229
// overwritten by this store.
230+
// Requiring that the projection to the partial store path is materializable guarantees
231+
// that we can split the store.
219232
potentiallyDeadSubpath = successivePath
220233
}
221234
case is DeallocRefInst, is DeallocStackRefInst, is DeallocBoxInst:
@@ -241,6 +254,8 @@ private struct InstructionScanner {
241254
if inst.mayRead(fromAddress: storeAddress, aliasAnalysis) {
242255
return .alive
243256
}
257+
// TODO: We might detect that this is a partial read of the original store which potentially
258+
// enables partial dead store elimination.
244259
}
245260
}
246261
return .transparent

SwiftCompilerSources/Sources/Optimizer/Utilities/AccessUtils.swift

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,11 @@ enum AccessBase : CustomStringConvertible, Hashable {
162162
}
163163
}
164164

165-
// Returns true if it's guaranteed that this access has the same base address as the `other` access.
165+
/// Returns true if it's guaranteed that this access has the same base address as the `other` access.
166+
///
167+
/// `isEqual` abstracts away the projection instructions that are included as part of the AccessBase:
168+
/// multiple `project_box` and `ref_element_addr` instructions are equivalent bases as long as they
169+
/// refer to the same variable or class property.
166170
func isEqual(to other: AccessBase) -> Bool {
167171
switch (self, other) {
168172
case (.box(let pb1), .box(let pb2)):
@@ -275,11 +279,52 @@ struct AccessPath : CustomStringConvertible {
275279
return false
276280
}
277281

278-
func isEqualOrOverlaps(_ other: AccessPath) -> Bool {
279-
return base.isEqual(to: other.base) &&
280-
// Note: an access with a smaller path overlaps an access with larger path, e.g.
281-
// `s0` overlaps `s0.s1`
282-
projectionPath.isSubPath(of: other.projectionPath)
282+
/// Returns true if this access addresses the same memory location as `other` or if `other`
283+
/// is a sub-field of this access.
284+
285+
/// Note that this access _contains_ `other` if `other` has a _larger_ projection path than this acccess.
286+
/// For example:
287+
/// `%value.s0` contains `%value.s0.s1`
288+
func isEqualOrContains(_ other: AccessPath) -> Bool {
289+
return getProjection(to: other) != nil
290+
}
291+
292+
/// Returns the projection path to `other` if this access path is equal or contains `other`.
293+
///
294+
/// For example,
295+
/// `%value.s0`.getProjection(to: `%value.s0.s1`)
296+
/// yields
297+
/// `s1`
298+
func getProjection(to other: AccessPath) -> SmallProjectionPath? {
299+
if !base.isEqual(to: other.base) {
300+
return nil
301+
}
302+
return projectionPath.subtract(from: other.projectionPath)
303+
}
304+
305+
/// Like `getProjection`, but also requires that the resulting projection path is materializable.
306+
func getMaterializableProjection(to other: AccessPath) -> SmallProjectionPath? {
307+
if let projectionPath = getProjection(to: other),
308+
projectionPath.isMaterializable {
309+
return projectionPath
310+
}
311+
return nil
312+
}
313+
}
314+
315+
private extension SmallProjectionPath {
316+
/// Returns true if the path only contains projections which can be materialized as
317+
/// SIL struct or tuple projection instructions - for values or addresses.
318+
var isMaterializable: Bool {
319+
let (kind, _, subPath) = pop()
320+
switch kind {
321+
case .root:
322+
return true
323+
case .structField, .tupleField:
324+
return subPath.isMaterializable
325+
default:
326+
return false
327+
}
283328
}
284329
}
285330

SwiftCompilerSources/Sources/SIL/SmallProjectionPath.swift

Lines changed: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public struct SmallProjectionPath : Hashable, CustomStringConvertible, NoReflect
7373
case enumCase = 0x3 // A concrete enum case (with payload): syntax e.g. `e4'
7474
case classField = 0x4 // A concrete class field: syntax e.g. `c1`
7575
case indexedElement = 0x5 // A constant offset into an array of elements: syntax e.g. 'i2'
76-
// The index must not be 0 and there must not be two successive element indices in the path.
76+
// The index must be greater than 0 and there must not be two successive element indices in the path.
7777

7878
// "Large" kinds: starting from here the low 3 bits must be 1.
7979
// This and all following kinds (we'll add in the future) cannot have a field index.
@@ -200,12 +200,18 @@ public struct SmallProjectionPath : Hashable, CustomStringConvertible, NoReflect
200200
public func push(_ kind: FieldKind, index: Int = 0) -> SmallProjectionPath {
201201
assert(kind != .anything || bytes == 0, "'anything' only allowed in last path component")
202202
if (kind.isIndexedElement) {
203-
if kind == .indexedElement && index == 0 {
204-
// Ignore zero indices
205-
return self
203+
let (k, i, numBits) = top
204+
if kind == .indexedElement {
205+
if index == 0 {
206+
// Ignore zero indices
207+
return self
208+
}
209+
if k == .indexedElement {
210+
// "Merge" two constant successive indexed elements
211+
return pop(numBits: numBits).push(.indexedElement, index: index + i)
212+
}
206213
}
207-
// "Merge" two successive indexed elements
208-
let (k, _, numBits) = top
214+
// "Merge" two successive indexed elements which doesn't have a constant result
209215
if (k.isIndexedElement) {
210216
return pop(numBits: numBits).push(.anyIndexedElement)
211217
}
@@ -474,26 +480,26 @@ public struct SmallProjectionPath : Hashable, CustomStringConvertible, NoReflect
474480
return false
475481
}
476482

477-
/// Return true if this path is a sub-path of `rhs` or is equivalent to `rhs`.
483+
/// Subtracts this path from a larger path if this path is a prefix of the other path.
478484
///
479485
/// For example:
480-
/// `s0` is a sub-path of `s0.s1`
481-
/// `s0` is not a sub-path of `s1`
482-
/// `s0.s1` is a sub-path of `s0.s1`
483-
/// `i*.s1` is not a sub-path of `i*.s1` because the actual field is unknown on both sides
484-
public func isSubPath(of rhs: SmallProjectionPath) -> Bool {
486+
/// subtracting `s0` from `s0.s1` yields `s1`
487+
/// subtracting `s0` from `s1` yields nil, because `s0` is not a prefix of `s1`
488+
/// subtracting `s0.s1` from `s0.s1` yields an empty path
489+
/// subtracting `i*.s1` from `i*.s1` yields nil, because the actual index is unknown on both sides
490+
public func subtract(from rhs: SmallProjectionPath) -> SmallProjectionPath? {
485491
let (lhsKind, lhsIdx, lhsBits) = top
486492
switch lhsKind {
487493
case .root:
488-
return true
494+
return rhs
489495
case .classField, .tailElements, .structField, .tupleField, .enumCase, .existential, .indexedElement:
490496
let (rhsKind, rhsIdx, rhsBits) = rhs.top
491497
if lhsKind == rhsKind && lhsIdx == rhsIdx {
492-
return pop(numBits: lhsBits).isSubPath(of: rhs.pop(numBits: rhsBits))
498+
return pop(numBits: lhsBits).subtract(from: rhs.pop(numBits: rhsBits))
493499
}
494-
return false
500+
return nil
495501
case .anything, .anyValueFields, .anyClassField, .anyIndexedElement:
496-
return false
502+
return nil
497503
}
498504
}
499505
}
@@ -632,9 +638,9 @@ extension SmallProjectionPath {
632638
basicPushPop()
633639
parsing()
634640
merging()
641+
subtracting()
635642
matching()
636643
overlapping()
637-
subPathTesting()
638644
predicates()
639645
path2path()
640646

@@ -652,9 +658,15 @@ extension SmallProjectionPath {
652658
assert(p5.pop().path.isEmpty)
653659
let p6 = SmallProjectionPath(.indexedElement, index: 1).push(.indexedElement, index: 2)
654660
let (k6, i6, p7) = p6.pop()
655-
assert(k6 == .anyIndexedElement && i6 == 0 && p7.isEmpty)
661+
assert(k6 == .indexedElement && i6 == 3 && p7.isEmpty)
656662
let p8 = SmallProjectionPath(.indexedElement, index: 0)
657663
assert(p8.isEmpty)
664+
let p9 = SmallProjectionPath(.indexedElement, index: 1).push(.anyIndexedElement)
665+
let (k9, i9, p10) = p9.pop()
666+
assert(k9 == .anyIndexedElement && i9 == 0 && p10.isEmpty)
667+
let p11 = SmallProjectionPath(.anyIndexedElement).push(.indexedElement, index: 1)
668+
let (k11, i11, p12) = p11.pop()
669+
assert(k11 == .anyIndexedElement && i11 == 0 && p12.isEmpty)
658670
}
659671

660672
func parsing() {
@@ -727,6 +739,35 @@ extension SmallProjectionPath {
727739
assert(result2 == expect)
728740
}
729741

742+
func subtracting() {
743+
testSubtract("s0", "s0.s1", expect: "s1")
744+
testSubtract("s0", "s1", expect: nil)
745+
testSubtract("s0.s1", "s0.s1", expect: "")
746+
testSubtract("i*.s1", "i*.s1", expect: nil)
747+
testSubtract("ct.s1.0.i3.x", "ct.s1.0.i3.x", expect: "")
748+
testSubtract("c0.s1.0.i3", "c0.s1.0.i3.x", expect: "x")
749+
testSubtract("s1.0.i3.x", "s1.0.i3", expect: nil)
750+
testSubtract("v**.s1", "v**.s1", expect: nil)
751+
testSubtract("i*", "i*", expect: nil)
752+
}
753+
754+
func testSubtract(_ lhsStr: String, _ rhsStr: String, expect expectStr: String?) {
755+
var lhsParser = StringParser(lhsStr)
756+
let lhs = try! lhsParser.parseProjectionPathFromSIL()
757+
var rhsParser = StringParser(rhsStr)
758+
let rhs = try! rhsParser.parseProjectionPathFromSIL()
759+
760+
let result = lhs.subtract(from: rhs)
761+
762+
if let expectStr = expectStr {
763+
var expectParser = StringParser(expectStr)
764+
let expect = try! expectParser.parseProjectionPathFromSIL()
765+
assert(result! == expect)
766+
} else {
767+
assert(result == nil)
768+
}
769+
}
770+
730771
func matching() {
731772
testMatch("ct", "c*", expect: true)
732773
testMatch("c1", "c*", expect: true)
@@ -799,27 +840,6 @@ extension SmallProjectionPath {
799840
assert(reversedResult == expect)
800841
}
801842

802-
func subPathTesting() {
803-
testSubPath("s0", "s0.s1", expect: true)
804-
testSubPath("s0", "s1", expect: false)
805-
testSubPath("s0.s1", "s0.s1", expect: true)
806-
testSubPath("i*.s1", "i*.s1", expect: false)
807-
testSubPath("ct.s1.0.i3.x", "ct.s1.0.i3.x", expect: true)
808-
testSubPath("c0.s1.0.i3", "c0.s1.0.i3.x", expect: true)
809-
testSubPath("s1.0.i3.x", "s1.0.i3", expect: false)
810-
testSubPath("v**.s1", "v**.s1", expect: false)
811-
testSubPath("i*", "i*", expect: false)
812-
}
813-
814-
func testSubPath(_ lhsStr: String, _ rhsStr: String, expect: Bool) {
815-
var lhsParser = StringParser(lhsStr)
816-
let lhs = try! lhsParser.parseProjectionPathFromSIL()
817-
var rhsParser = StringParser(rhsStr)
818-
let rhs = try! rhsParser.parseProjectionPathFromSIL()
819-
let result = lhs.isSubPath(of: rhs)
820-
assert(result == expect)
821-
}
822-
823843
func predicates() {
824844
testPredicate("v**", \.hasClassProjection, expect: false)
825845
testPredicate("v**.c0.s1.v**", \.hasClassProjection, expect: true)

0 commit comments

Comments
 (0)