From b03ef3cc80dc9beda92c6562c70caa54aeaf740e Mon Sep 17 00:00:00 2001 From: Erik Eckstein Date: Fri, 30 Jun 2023 11:31:11 +0200 Subject: [PATCH 01/10] AliasAnalysis: rename the main API functions Instead of aliasAnalysis.mayRead(inst: i, fromAddress: a) it's more natural to write i.mayRead(fromAddress: a, aliasAnalysis) --- .../Optimizer/Analysis/AliasAnalysis.swift | 56 ++++++++++--------- .../TestPasses/MemBehaviorDumper.swift | 4 +- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/SwiftCompilerSources/Sources/Optimizer/Analysis/AliasAnalysis.swift b/SwiftCompilerSources/Sources/Optimizer/Analysis/AliasAnalysis.swift index b265821a5ba9d..f7695822f9051 100644 --- a/SwiftCompilerSources/Sources/Optimizer/Analysis/AliasAnalysis.swift +++ b/SwiftCompilerSources/Sources/Optimizer/Analysis/AliasAnalysis.swift @@ -16,33 +16,6 @@ import SIL struct AliasAnalysis { let bridged: BridgedAliasAnalysis - func mayRead(_ inst: Instruction, fromAddress: Value) -> Bool { - switch bridged.getMemBehavior(inst.bridged, fromAddress.bridged) { - case .MayRead, .MayReadWrite, .MayHaveSideEffects: - return true - default: - return false - } - } - - func mayWrite(_ inst: Instruction, toAddress: Value) -> Bool { - switch bridged.getMemBehavior(inst.bridged, toAddress.bridged) { - case .MayWrite, .MayReadWrite, .MayHaveSideEffects: - return true - default: - return false - } - } - - func mayReadOrWrite(_ inst: Instruction, address: Value) -> Bool { - switch bridged.getMemBehavior(inst.bridged, address.bridged) { - case .MayRead, .MayWrite, .MayReadWrite, .MayHaveSideEffects: - return true - default: - return false - } - } - /// Returns the correct path for address-alias functions. static func getPtrOrAddressPath(for value: Value) -> SmallProjectionPath { let ty = value.type @@ -119,6 +92,35 @@ struct AliasAnalysis { } } +extension Instruction { + func mayRead(fromAddress: Value, _ aliasAnalysis: AliasAnalysis) -> Bool { + switch aliasAnalysis.bridged.getMemBehavior(bridged, fromAddress.bridged) { + case .MayRead, .MayReadWrite, .MayHaveSideEffects: + return true + default: + return false + } + } + + func mayWrite(toAddress: Value, _ aliasAnalysis: AliasAnalysis) -> Bool { + switch aliasAnalysis.bridged.getMemBehavior(bridged, toAddress.bridged) { + case .MayWrite, .MayReadWrite, .MayHaveSideEffects: + return true + default: + return false + } + } + + func mayReadOrWrite(address: Value, _ aliasAnalysis: AliasAnalysis) -> Bool { + switch aliasAnalysis.bridged.getMemBehavior(bridged, address.bridged) { + case .MayRead, .MayWrite, .MayReadWrite, .MayHaveSideEffects: + return true + default: + return false + } + } +} + private func getMemoryEffect(ofApply apply: ApplySite, for address: Value, path: SmallProjectionPath, _ context: FunctionPassContext) -> SideEffects.Memory { let calleeAnalysis = context.calleeAnalysis let visitor = SideEffectsVisitor(apply: apply, calleeAnalysis: calleeAnalysis, isAddress: true) diff --git a/SwiftCompilerSources/Sources/Optimizer/TestPasses/MemBehaviorDumper.swift b/SwiftCompilerSources/Sources/Optimizer/TestPasses/MemBehaviorDumper.swift index 091148039708b..21b6600a18ea3 100644 --- a/SwiftCompilerSources/Sources/Optimizer/TestPasses/MemBehaviorDumper.swift +++ b/SwiftCompilerSources/Sources/Optimizer/TestPasses/MemBehaviorDumper.swift @@ -28,8 +28,8 @@ let memBehaviorDumper = FunctionPass(name: "dump-mem-behavior") { for value in values where value.definingInstruction != inst { if value.type.isAddress || value is AddressToPointerInst { - let read = aliasAnalysis.mayRead(inst, fromAddress: value) - let write = aliasAnalysis.mayWrite(inst, toAddress: value) + let read = inst.mayRead(fromAddress: value, aliasAnalysis) + let write = inst.mayWrite(toAddress: value, aliasAnalysis) print("PAIR #\(currentPair).") print(" \(inst)") print(" \(value)") From 0d3fe6382e4a47f571c91f701f65990844c3fbb4 Mon Sep 17 00:00:00 2001 From: Erik Eckstein Date: Wed, 5 Jul 2023 14:13:52 +0200 Subject: [PATCH 02/10] SmallProjectionPath: model existential projections and indexed elements add field types: `x`: existential address projection, like `open_existential_addr` `i`: indexed element with constant index, like `index_addr` with an integer literal as index `i*`: indexed element with an unknown index --- .../Sources/SIL/SmallProjectionPath.swift | 154 ++++++++++++++++-- 1 file changed, 144 insertions(+), 10 deletions(-) diff --git a/SwiftCompilerSources/Sources/SIL/SmallProjectionPath.swift b/SwiftCompilerSources/Sources/SIL/SmallProjectionPath.swift index 8c575adba3765..955dbe887cf75 100644 --- a/SwiftCompilerSources/Sources/SIL/SmallProjectionPath.swift +++ b/SwiftCompilerSources/Sources/SIL/SmallProjectionPath.swift @@ -72,17 +72,22 @@ public struct SmallProjectionPath : Hashable, CustomStringConvertible, NoReflect case tupleField = 0x2 // A concrete tuple element: syntax e.g. `2` case enumCase = 0x3 // A concrete enum case (with payload): syntax e.g. `e4' case classField = 0x4 // A concrete class field: syntax e.g. `c1` - case tailElements = 0x5 // A tail allocated element of a class: syntax `ct` - case anyValueFields = 0x6 // Any number of any value fields (struct, tuple, enum): syntax `v**` + case indexedElement = 0x5 // A constant offset into an array of elements: syntax e.g. 'i2' + // The index must not be 0 and there must not be two successive element indices in the path. // "Large" kinds: starting from here the low 3 bits must be 1. // This and all following kinds (we'll add in the future) cannot have a field index. - case anyClassField = 0x7 // Any class field, including tail elements: syntax `c*` - case anything = 0xf // Any number of any fields: syntax `**` + case tailElements = 0x07 // (0 << 3) | 0x7 A tail allocated element of a class: syntax `ct` + case existential = 0x0f // (1 << 3) | 0x7 A concrete value projected out of an existential: synatx 'x' + case anyClassField = 0x17 // (2 << 3) | 0x7 Any class field, including tail elements: syntax `c*` + case anyIndexedElement = 0x1f // (3 << 3) | 0x7 An unknown offset into an array of elements. + // There must not be two successive element indices in the path. + case anyValueFields = 0x27 // (4 << 3) | 0x7 Any number of any value fields (struct, tuple, enum): syntax `v**` + case anything = 0x2f // (5 << 3) | 0x7 Any number of any fields: syntax `**` public var isValueField: Bool { switch self { - case .anyValueFields, .structField, .tupleField, .enumCase: + case .anyValueFields, .structField, .tupleField, .enumCase, .indexedElement, .anyIndexedElement, .existential: return true case .root, .anything, .anyClassField, .classField, .tailElements: return false @@ -93,10 +98,19 @@ public struct SmallProjectionPath : Hashable, CustomStringConvertible, NoReflect switch self { case .anyClassField, .classField, .tailElements: return true - case .root, .anything, .anyValueFields, .structField, .tupleField, .enumCase: + case .root, .anything, .anyValueFields, .structField, .tupleField, .enumCase, .indexedElement, .anyIndexedElement, .existential: return false } } + + var isIndexedElement: Bool { + switch self { + case .anyIndexedElement, .indexedElement: + return true + default: + return false + } + } } public init() { self.bytes = 0 } @@ -121,6 +135,9 @@ public struct SmallProjectionPath : Hashable, CustomStringConvertible, NoReflect case .enumCase: s = "e\(idx)" case .classField: s = "c\(idx)" case .tailElements: s = "ct" + case .existential: s = "x" + case .indexedElement: s = "i\(idx)" + case .anyIndexedElement: s = "i*" case .anything: s = "**" case .anyValueFields: s = "v**" case .anyClassField: s = "c*" @@ -182,6 +199,17 @@ public struct SmallProjectionPath : Hashable, CustomStringConvertible, NoReflect /// For example, pushing `s0` to `c3.e1` returns `s0.c3.e1`. public func push(_ kind: FieldKind, index: Int = 0) -> SmallProjectionPath { assert(kind != .anything || bytes == 0, "'anything' only allowed in last path component") + if (kind.isIndexedElement) { + if kind == .indexedElement && index == 0 { + // Ignore zero indices + return self + } + // "Merge" two successive indexed elements + let (k, _, numBits) = top + if (k.isIndexedElement) { + return pop(numBits: numBits).push(.anyIndexedElement) + } + } var idx = index var b = bytes if (b >> 56) != 0 { @@ -234,6 +262,11 @@ public struct SmallProjectionPath : Hashable, CustomStringConvertible, NoReflect return pop(numBits: numBits) } return nil + case .anyIndexedElement: + if kind.isIndexedElement { + return self + } + return pop(numBits: numBits).popIfMatches(kind, index: index) case kind: if let i = index { if i != idx { return nil } @@ -294,6 +327,15 @@ public struct SmallProjectionPath : Hashable, CustomStringConvertible, NoReflect } } + public func popIndexedElements() -> SmallProjectionPath { + var p = self + while true { + let (k, _, numBits) = p.top + if !k.isIndexedElement { return p } + p = p.pop(numBits: numBits) + } + } + /// Pops the last class projection and all following value fields from the tail of the path. /// For example: /// `s0.e2.3.c4.s1` -> `s0.e2.3` @@ -344,7 +386,9 @@ public struct SmallProjectionPath : Hashable, CustomStringConvertible, NoReflect let (kind, _, subPath) = pop() if !kind.isClassField { return false } return subPath.matches(pattern: subPattern) - case .structField, .tupleField, .enumCase, .classField, .tailElements: + case .anyIndexedElement: + return popIndexedElements().matches(pattern: subPattern) + case .structField, .tupleField, .enumCase, .classField, .tailElements, .indexedElement, .existential: let (kind, index, subPath) = pop() if kind != patternKind || index != patternIdx { return false } return subPath.matches(pattern: subPattern) @@ -373,6 +417,15 @@ public struct SmallProjectionPath : Hashable, CustomStringConvertible, NoReflect } return subPath.push(lhsKind, index: lhsIdx) } + if lhsKind.isIndexedElement || rhsKind.isIndexedElement { + let subPath = popIndexedElements().merge(with: rhs.popIndexedElements()) + let subPathTopKind = subPath.top.kind + assert(!subPathTopKind.isIndexedElement) + if subPathTopKind == .anything || subPathTopKind == .anyValueFields { + return subPath + } + return subPath.push(.anyIndexedElement) + } if lhsKind.isValueField || rhsKind.isValueField { let subPath = popAllValueFields().merge(with: rhs.popAllValueFields()) assert(!subPath.top.kind.isValueField) @@ -407,6 +460,9 @@ public struct SmallProjectionPath : Hashable, CustomStringConvertible, NoReflect if lhsKind == .anything || rhsKind == .anything { return true } + if lhsKind == .anyIndexedElement || rhsKind == .anyIndexedElement { + return popIndexedElements().mayOverlap(with: rhs.popIndexedElements()) + } if lhsKind == .anyValueFields || rhsKind == .anyValueFields { return popAllValueFields().mayOverlap(with: rhs.popAllValueFields()) } @@ -417,6 +473,29 @@ public struct SmallProjectionPath : Hashable, CustomStringConvertible, NoReflect } return false } + + /// Return true if this path is a sub-path of `rhs` or is equivalent to `rhs`. + /// + /// For example: + /// `s0` is a sub-path of `s0.s1` + /// `s0` is not a sub-path of `s1` + /// `s0.s1` is a sub-path of `s0.s1` + /// `i*.s1` is not a sub-path of `i*.s1` because the actual field is unknown on both sides + public func isSubPath(of rhs: SmallProjectionPath) -> Bool { + let (lhsKind, lhsIdx, lhsBits) = top + switch lhsKind { + case .root: + return true + case .classField, .tailElements, .structField, .tupleField, .enumCase, .existential, .indexedElement: + let (rhsKind, rhsIdx, rhsBits) = rhs.top + if lhsKind == rhsKind && lhsIdx == rhsIdx { + return pop(numBits: lhsBits).isSubPath(of: rhs.pop(numBits: rhsBits)) + } + return false + case .anything, .anyValueFields, .anyClassField, .anyIndexedElement: + return false + } + } } //===----------------------------------------------------------------------===// @@ -490,8 +569,12 @@ extension StringParser { entries.append((.anyClassField, 0)) } else if consume("v**") { entries.append((.anyValueFields, 0)) + } else if consume("i*") { + entries.append((.anyIndexedElement, 0)) } else if consume("ct") { entries.append((.tailElements, 0)) + } else if consume("x") { + entries.append((.existential, 0)) } else if consume("c") { guard let idx = consumeInt(withWhiteSpace: false) else { try throwError("expected class field index") @@ -507,6 +590,11 @@ extension StringParser { try throwError("expected struct field index") } entries.append((.structField, idx)) + } else if consume("i") { + guard let idx = consumeInt(withWhiteSpace: false) else { + try throwError("expected index") + } + entries.append((.indexedElement, idx)) } else if let tupleElemIdx = consumeInt() { entries.append((.tupleField, tupleElemIdx)) } else if !consume(".") { @@ -546,6 +634,7 @@ extension SmallProjectionPath { merging() matching() overlapping() + subPathTesting() predicates() path2path() @@ -561,6 +650,11 @@ extension SmallProjectionPath { assert(k4 == .enumCase && i4 == 876) let p5 = SmallProjectionPath(.anything) assert(p5.pop().path.isEmpty) + let p6 = SmallProjectionPath(.indexedElement, index: 1).push(.indexedElement, index: 2) + let (k6, i6, p7) = p6.pop() + assert(k6 == .anyIndexedElement && i6 == 0 && p7.isEmpty) + let p8 = SmallProjectionPath(.indexedElement, index: 0) + assert(p8.isEmpty) } func parsing() { @@ -575,7 +669,10 @@ extension SmallProjectionPath { .push(.enumCase, index: 6) .push(.anyClassField) .push(.tupleField, index: 2)) - + testParse("i3.x.i*", expect: SmallProjectionPath(.anyIndexedElement) + .push(.existential) + .push(.indexedElement, index: 3)) + do { var parser = StringParser("c*.s123.s3.s123.s3.s123.s3.s123.s3.s123.s3.s123.s3.s123.s3.s123.s3.s123.s3.s123.s3.s123.s3.s123.s3.s123.s3.**") _ = try parser.parseProjectionPathFromSIL() @@ -606,6 +703,10 @@ extension SmallProjectionPath { testMerge("s1.s1.c2", "s1.c2", expect: "s1.v**.c2") testMerge("s1.s0", "s2.s0", expect: "v**") testMerge("ct", "c2", expect: "c*") + testMerge("i1", "i2", expect: "i*") + testMerge("i*", "i2", expect: "i*") + testMerge("s0.i*.e3", "s0.e3", expect: "s0.i*.e3") + testMerge("i*", "v**", expect: "v**") testMerge("ct.s0.e0.v**.c0", "ct.s0.e0.v**.c0", expect: "ct.s0.e0.v**.c0") testMerge("ct.s0.s0.c0", "ct.s0.e0.s0.c0", expect: "ct.s0.v**.c0") @@ -635,18 +736,22 @@ extension SmallProjectionPath { testMatch("c*", "c1", expect: false) testMatch("c*", "ct", expect: false) testMatch("v**", "s0", expect: false) + testMatch("i1", "i1", expect: true) + testMatch("i1", "i*", expect: true) + testMatch("i*", "i1", expect: false) testMatch("s0.s1", "s0.s1", expect: true) testMatch("s0.s2", "s0.s1", expect: false) testMatch("s0", "s0.v**", expect: true) testMatch("s0.s1", "s0.v**", expect: true) testMatch("s0.1.e2", "s0.v**", expect: true) - testMatch("s0.v**.e2", "v**", expect: true) + testMatch("s0.v**.x.e2", "v**", expect: true) testMatch("s0.v**", "s0.s1", expect: false) testMatch("s0.s1.c*", "s0.v**", expect: false) testMatch("s0.v**", "s0.**", expect: true) testMatch("s1.v**", "s0.**", expect: false) testMatch("s0.**", "s0.v**", expect: false) + testMatch("s0.s1", "s0.i*.s1", expect: true) } func testMatch(_ lhsStr: String, _ rhsStr: String, expect: Bool) { @@ -670,13 +775,17 @@ extension SmallProjectionPath { testOverlap("s0.c*.s2", "s0.c1.c2.s2", expect: false) testOverlap("s0.c*.s2", "s0.s2", expect: false) - testOverlap("s0.v**.s2", "s0.s3", expect: true) + testOverlap("s0.v**.s2", "s0.s3.x", expect: true) testOverlap("s0.v**.s2.c2", "s0.s3.c1", expect: false) testOverlap("s0.v**.s2", "s1.s3", expect: false) testOverlap("s0.v**.s2", "s0.v**.s3", expect: true) testOverlap("s0.**", "s0.s3.c1", expect: true) testOverlap("**", "s0.s3.c1", expect: true) + + testOverlap("i1", "i*", expect: true) + testOverlap("i1", "v**", expect: true) + testOverlap("s0.i*.s1", "s0.s1", expect: true) } func testOverlap(_ lhsStr: String, _ rhsStr: String, expect: Bool) { @@ -690,6 +799,27 @@ extension SmallProjectionPath { assert(reversedResult == expect) } + func subPathTesting() { + testSubPath("s0", "s0.s1", expect: true) + testSubPath("s0", "s1", expect: false) + testSubPath("s0.s1", "s0.s1", expect: true) + testSubPath("i*.s1", "i*.s1", expect: false) + testSubPath("ct.s1.0.i3.x", "ct.s1.0.i3.x", expect: true) + testSubPath("c0.s1.0.i3", "c0.s1.0.i3.x", expect: true) + testSubPath("s1.0.i3.x", "s1.0.i3", expect: false) + testSubPath("v**.s1", "v**.s1", expect: false) + testSubPath("i*", "i*", expect: false) + } + + func testSubPath(_ lhsStr: String, _ rhsStr: String, expect: Bool) { + var lhsParser = StringParser(lhsStr) + let lhs = try! lhsParser.parseProjectionPathFromSIL() + var rhsParser = StringParser(rhsStr) + let rhs = try! rhsParser.parseProjectionPathFromSIL() + let result = lhs.isSubPath(of: rhs) + assert(result == expect) + } + func predicates() { testPredicate("v**", \.hasClassProjection, expect: false) testPredicate("v**.c0.s1.v**", \.hasClassProjection, expect: true) @@ -737,6 +867,10 @@ extension SmallProjectionPath { testPath2Path("c0.s3", { $0.popIfMatches(.anyClassField) }, expect: nil) testPath2Path("**", { $0.popIfMatches(.anyClassField) }, expect: "**") testPath2Path("c*.e3", { $0.popIfMatches(.anyClassField) }, expect: "e3") + + testPath2Path("i*.e3.s0", { $0.popIfMatches(.enumCase, index: 3) }, expect: "s0") + testPath2Path("i1.e3.s0", { $0.popIfMatches(.enumCase, index: 3) }, expect: nil) + testPath2Path("i*.e3.s0", { $0.popIfMatches(.indexedElement, index: 0) }, expect: "i*.e3.s0") } func testPath2Path(_ pathStr: String, _ transform: (SmallProjectionPath) -> SmallProjectionPath?, expect: String?) { From 8bb3e45b416d21872672102e1752e773d1f6e532 Mon Sep 17 00:00:00 2001 From: Erik Eckstein Date: Wed, 5 Jul 2023 14:55:00 +0200 Subject: [PATCH 03/10] WalkUtils: more precise handling of existential projections and `index_addr` --- .../Optimizer/Utilities/EscapeUtils.swift | 5 +- .../Optimizer/Utilities/WalkUtils.swift | 77 ++++++++++++++----- test/SILOptimizer/addr_escape_info.sil | 76 ++++++++++++++++++ test/SILOptimizer/escape_info.sil | 2 +- 4 files changed, 136 insertions(+), 24 deletions(-) diff --git a/SwiftCompilerSources/Sources/Optimizer/Utilities/EscapeUtils.swift b/SwiftCompilerSources/Sources/Optimizer/Utilities/EscapeUtils.swift index 04d57d373e345..2ef368adb5cd3 100644 --- a/SwiftCompilerSources/Sources/Optimizer/Utilities/EscapeUtils.swift +++ b/SwiftCompilerSources/Sources/Optimizer/Utilities/EscapeUtils.swift @@ -489,9 +489,6 @@ fileprivate struct EscapeWalker : ValueDefUseWalker, return walkDownUses(ofValue: svi, path: path.with(knownType: nil)) case let atp as AddressToPointerInst: return walkDownUses(ofValue: atp, path: path.with(knownType: nil)) - case let ia as IndexAddrInst: - assert(operand.index == 0) - return walkDownUses(ofAddress: ia, path: path.with(knownType: nil)) case is DeallocStackInst, is InjectEnumAddrInst, is FixLifetimeInst, is EndBorrowInst, is EndAccessInst, is DebugValueInst: return .continueWalk @@ -741,7 +738,7 @@ fileprivate struct EscapeWalker : ValueDefUseWalker, } else { return isEscaping } - case is PointerToAddressInst, is IndexAddrInst: + case is PointerToAddressInst: return walkUp(value: (def as! SingleValueInstruction).operands[0].value, path: path.with(knownType: nil)) case let rta as RefTailAddrInst: return walkUp(value: rta.instance, path: path.push(.tailElements, index: 0).with(knownType: nil)) diff --git a/SwiftCompilerSources/Sources/Optimizer/Utilities/WalkUtils.swift b/SwiftCompilerSources/Sources/Optimizer/Utilities/WalkUtils.swift index ed5a77bd55f16..2fe156d05008e 100644 --- a/SwiftCompilerSources/Sources/Optimizer/Utilities/WalkUtils.swift +++ b/SwiftCompilerSources/Sources/Optimizer/Utilities/WalkUtils.swift @@ -340,8 +340,15 @@ extension ValueDefUseWalker { } else { return unmatchedPath(value: operand, path: path) } - case is InitExistentialRefInst, is OpenExistentialRefInst, - is BeginBorrowInst, is CopyValueInst, is MoveValueInst, + case let ier as InitExistentialRefInst: + return walkDownUses(ofValue: ier, path: path.push(.existential, index: 0)) + case let oer as OpenExistentialRefInst: + if let path = path.popIfMatches(.existential, index: 0) { + return walkDownUses(ofValue: oer, path: path) + } else { + return unmatchedPath(value: operand, path: path) + } + case is BeginBorrowInst, is CopyValueInst, is MoveValueInst, is UpcastInst, is UncheckedRefCastInst, is EndCOWMutationInst, is RefToBridgeObjectInst, is BridgeObjectToRefInst, is MarkMustCheckInst: return walkDownUses(ofValue: (instruction as! SingleValueInstruction), path: path) @@ -470,14 +477,23 @@ extension AddressDefUseWalker { } else { return unmatchedPath(address: operand, path: path) } - case is InitExistentialAddrInst, is OpenExistentialAddrInst, - is IndexAddrInst, is MarkMustCheckInst: - // FIXME: for now `index_addr` is treated as a forwarding instruction since - // SmallProjectionPath does not track indices. - // This is ok since `index_addr` is eventually preceeded by a `tail_addr` - // which has pushed a `"ct"` component on the path that matches any - // `index_addr` address. - return walkDownUses(ofAddress: instruction as! SingleValueInstruction, path: path) + case is InitExistentialAddrInst, is OpenExistentialAddrInst: + if let path = path.popIfMatches(.existential, index: 0) { + return walkDownUses(ofAddress: instruction as! SingleValueInstruction, path: path) + } else { + return unmatchedPath(address: operand, path: path) + } + case let ia as IndexAddrInst: + if let (pathIdx, subPath) = path.pop(kind: .indexedElement) { + if let idx = ia.constantSmallIndex, + idx == pathIdx { + return walkDownUses(ofAddress: ia, path: subPath) + } + return walkDownUses(ofAddress: ia, path: subPath.push(.anyIndexedElement, index: 0)) + } + return walkDownUses(ofAddress: ia, path: path) + case let mmc as MarkMustCheckInst: + return walkDownUses(ofAddress: mmc, path: path) case let ba as BeginAccessInst: // Don't treat `end_access` as leaf-use. Just ignore it. return walkDownNonEndAccessUses(of: ba, path: path) @@ -630,8 +646,15 @@ extension ValueUseDefWalker { } else { return rootDef(value: mvr, path: path) } - case is InitExistentialRefInst, is OpenExistentialRefInst, - is BeginBorrowInst, is CopyValueInst, is MoveValueInst, + case let ier as InitExistentialRefInst: + if let path = path.popIfMatches(.existential, index: 0) { + return walkUp(value: ier.instance, path: path) + } else { + return unmatchedPath(value: ier, path: path) + } + case let oer as OpenExistentialRefInst: + return walkUp(value: oer.existential, path: path.push(.existential, index: 0)) + case is BeginBorrowInst, is CopyValueInst, is MoveValueInst, is UpcastInst, is UncheckedRefCastInst, is EndCOWMutationInst, is RefToBridgeObjectInst, is BridgeObjectToRefInst, is MarkMustCheckInst: return walkUp(value: (def as! Instruction).operands[0].value, path: path) @@ -718,14 +741,16 @@ extension AddressUseDefWalker { case is InitEnumDataAddrInst, is UncheckedTakeEnumDataAddrInst: return walkUp(address: (def as! UnaryInstruction).operand.value, path: path.push(.enumCase, index: (def as! EnumInstruction).caseIndex)) - case is InitExistentialAddrInst, is OpenExistentialAddrInst, is BeginAccessInst, is IndexAddrInst, - is MarkMustCheckInst: - // FIXME: for now `index_addr` is treated as a forwarding instruction since - // SmallProjectionPath does not track indices. - // This is ok since `index_addr` is eventually preceeded by a `tail_addr` - // which has pushed a `"ct"` component on the path that matches any - // `index_addr` address. + case is InitExistentialAddrInst, is OpenExistentialAddrInst: + return walkUp(address: (def as! Instruction).operands[0].value, path: path.push(.existential, index: 0)) + case is BeginAccessInst, is MarkMustCheckInst: return walkUp(address: (def as! Instruction).operands[0].value, path: path) + case let ia as IndexAddrInst: + if let idx = ia.constantSmallIndex { + return walkUp(address: ia.base, path: path.push(.indexedElement, index: idx)) + } else { + return walkUp(address: ia.base, path: path.push(.anyIndexedElement, index: 0)) + } case let mdi as MarkDependenceInst: return walkUp(address: mdi.operands[0].value, path: path) default: @@ -733,3 +758,17 @@ extension AddressUseDefWalker { } } } + +private extension IndexAddrInst { + var constantSmallIndex: Int? { + guard let literal = index as? IntegerLiteralInst else { + return nil + } + let index = literal.value + if index.isIntN(16) { + return Int(index.getSExtValue()) + } + return nil + } +} + diff --git a/test/SILOptimizer/addr_escape_info.sil b/test/SILOptimizer/addr_escape_info.sil index 5c5537cf54e7f..69f5cf44b5b06 100644 --- a/test/SILOptimizer/addr_escape_info.sil +++ b/test/SILOptimizer/addr_escape_info.sil @@ -31,6 +31,10 @@ class XandIntClass { @_hasStorage var i: Int } +protocol P {} + +extension Int : P {} + sil @no_arguments : $@convention(thin) () -> () sil @indirect_argument : $@convention(thin) (@in Int) -> () sil @indirect_struct_argument : $@convention(thin) (@in Str) -> () @@ -633,3 +637,75 @@ bb0: return %9 : $Int64 } +// CHECK-LABEL: Address escape information for test_index_addr: +// CHECK: pair 0 - 1 +// CHECK-NEXT: %2 = ref_tail_addr %1 : $X, $Int +// CHECK-NEXT: %5 = index_addr %2 : $*Int, %0 : $Builtin.Word +// CHECK-NEXT: may alias +// CHECK: pair 0 - 2 +// CHECK-NEXT: %2 = ref_tail_addr %1 : $X, $Int +// CHECK-NEXT: %6 = index_addr %2 : $*Int, %3 : $Builtin.Word +// CHECK-NEXT: may alias +// CHECK: pair 0 - 3 +// CHECK-NEXT: %2 = ref_tail_addr %1 : $X, $Int +// CHECK-NEXT: %7 = index_addr %2 : $*Int, %4 : $Builtin.Word +// CHECK-NEXT: may alias +// CHECK: pair 0 - 4 +// CHECK-NEXT: %2 = ref_tail_addr %1 : $X, $Int +// CHECK-NEXT: %8 = index_addr %7 : $*Int, %4 : $Builtin.Word +// CHECK-NEXT: may alias +// CHECK: pair 1 - 2 +// CHECK-NEXT: %5 = index_addr %2 : $*Int, %0 : $Builtin.Word +// CHECK-NEXT: %6 = index_addr %2 : $*Int, %3 : $Builtin.Word +// CHECK-NEXT: may alias +// CHECK: pair 1 - 3 +// CHECK-NEXT: %5 = index_addr %2 : $*Int, %0 : $Builtin.Word +// CHECK-NEXT: %7 = index_addr %2 : $*Int, %4 : $Builtin.Word +// CHECK-NEXT: may alias +// CHECK: pair 1 - 4 +// CHECK-NEXT: %5 = index_addr %2 : $*Int, %0 : $Builtin.Word +// CHECK-NEXT: %8 = index_addr %7 : $*Int, %4 : $Builtin.Word +// CHECK-NEXT: may alias +// CHECK: pair 2 - 4 +// CHECK-NEXT: %6 = index_addr %2 : $*Int, %3 : $Builtin.Word +// CHECK-NEXT: %8 = index_addr %7 : $*Int, %4 : $Builtin.Word +// CHECK-NEXT: may alias +// CHECK: End function test_index_addr +sil @test_index_addr : $@convention(thin) (Builtin.Word) -> () { +bb0(%0 : $Builtin.Word): + %1 = alloc_ref $X + %2 = ref_tail_addr %1 : $X, $Int + %3 = integer_literal $Builtin.Word, 2 + %4 = integer_literal $Builtin.Word, 1 + %5 = index_addr %2 : $*Int, %0 : $Builtin.Word + %6 = index_addr %2 : $*Int, %3 : $Builtin.Word + %7 = index_addr %2 : $*Int, %4 : $Builtin.Word + %8 = index_addr %7 : $*Int, %4 : $Builtin.Word + fix_lifetime %2 : $*Int + fix_lifetime %5 : $*Int + fix_lifetime %6 : $*Int + fix_lifetime %7 : $*Int + fix_lifetime %8 : $*Int + %r = tuple () + return %r : $() +} + +// CHECK-LABEL: Address escape information for test_existentials: +// CHECK: pair 0 - 1 +// CHECK-NEXT: %2 = init_existential_addr %1 : $*any P, $Int +// CHECK-NEXT: %4 = open_existential_addr immutable_access %1 : $*any P to $*@opened("D2149896-0C4D-11EE-92B7-0EA13E3AABB3", any P) Self +// CHECK-NEXT: may alias +// CHECK: End function test_existentials +sil @test_existentials : $@convention(thin) (Int) -> () { +bb0(%0 : $Int): + %1 = alloc_stack $P + %2 = init_existential_addr %1 : $*P, $Int + store %0 to %2 : $*Int + %4 = open_existential_addr immutable_access %1 : $*any P to $*@opened("D2149896-0C4D-11EE-92B7-0EA13E3AABB3", any P) Self + fix_lifetime %2 : $*Int + fix_lifetime %4 : $*@opened("D2149896-0C4D-11EE-92B7-0EA13E3AABB3", any P) Self + dealloc_stack %1 : $*P + %r = tuple () + return %r : $() +} + diff --git a/test/SILOptimizer/escape_info.sil b/test/SILOptimizer/escape_info.sil index f52b94509ccb4..81aa46f632072 100644 --- a/test/SILOptimizer/escape_info.sil +++ b/test/SILOptimizer/escape_info.sil @@ -615,7 +615,7 @@ bb0: // CHECK-LABEL: Escape information for test_cast_and_existentials_walk_down: -// CHECK: return[]: %0 = alloc_ref $Derived +// CHECK: return[x]: %0 = alloc_ref $Derived // CHECK: - : %1 = alloc_ref $Derived // CHECK: End function test_cast_and_existentials_walk_down sil @test_cast_and_existentials_walk_down : $@convention(thin) () -> ClassP { From 205c841ed877c53d9c49952f2accef8e6ea4a237 Mon Sep 17 00:00:00 2001 From: Erik Eckstein Date: Wed, 5 Jul 2023 14:57:00 +0200 Subject: [PATCH 04/10] Swift SIL: add some Builder APIs * `createStructExtract` * `createStructElementAddr` * `createTupleExtract` * `createTupleElementAddr` --- .../Sources/SIL/Builder.swift | 16 +++++++++++++ include/swift/SIL/SILBridging.h | 24 +++++++++++++++++++ include/swift/SIL/SILType.h | 2 ++ lib/SIL/IR/SILType.cpp | 10 +++++--- 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/SwiftCompilerSources/Sources/SIL/Builder.swift b/SwiftCompilerSources/Sources/SIL/Builder.swift index 47620d6b875de..5fedf6e17fbf8 100644 --- a/SwiftCompilerSources/Sources/SIL/Builder.swift +++ b/SwiftCompilerSources/Sources/SIL/Builder.swift @@ -250,6 +250,14 @@ public struct Builder { return notifyNew(structInst.getAs(StructInst.self)) } + public func createStructExtract(struct: Value, fieldIndex: Int) -> StructExtractInst { + return notifyNew(bridged.createStructExtract(`struct`.bridged, fieldIndex).getAs(StructExtractInst.self)) + } + + public func createStructElementAddr(structAddress: Value, fieldIndex: Int) -> StructElementAddrInst { + return notifyNew(bridged.createStructElementAddr(structAddress.bridged, fieldIndex).getAs(StructElementAddrInst.self)) + } + public func createTuple(type: Type, elements: [Value]) -> TupleInst { let tuple = elements.withBridgedValues { valuesRef in return bridged.createTuple(type.bridged, valuesRef) @@ -257,6 +265,14 @@ public struct Builder { return notifyNew(tuple.getAs(TupleInst.self)) } + public func createTupleExtract(tuple: Value, elementIndex: Int) -> TupleExtractInst { + return notifyNew(bridged.createTupleExtract(tuple.bridged, elementIndex).getAs(TupleExtractInst.self)) + } + + public func createTupleElementAddr(tupleAddress: Value, elementIndex: Int) -> TupleElementAddrInst { + return notifyNew(bridged.createTupleElementAddr(tupleAddress.bridged, elementIndex).getAs(TupleElementAddrInst.self)) + } + @discardableResult public func createStore(source: Value, destination: Value, ownership: StoreInst.Ownership) -> StoreInst { let store = bridged.createStore(source.bridged, destination.bridged, ownership.rawValue) diff --git a/include/swift/SIL/SILBridging.h b/include/swift/SIL/SILBridging.h index 4ea07bfe3c932..98188be3d7885 100644 --- a/include/swift/SIL/SILBridging.h +++ b/include/swift/SIL/SILBridging.h @@ -1268,12 +1268,36 @@ struct BridgedBuilder{ return {builder().createStruct(regularLoc(), type, elements.getValues(elementValues))}; } + SWIFT_IMPORT_UNSAFE + BridgedInstruction createStructExtract(BridgedValue str, SwiftInt fieldIndex) const { + swift::SILValue v = str.getSILValue(); + return {builder().createStructExtract(regularLoc(), v, v->getType().getFieldDecl(fieldIndex))}; + } + + SWIFT_IMPORT_UNSAFE + BridgedInstruction createStructElementAddr(BridgedValue addr, SwiftInt fieldIndex) const { + swift::SILValue v = addr.getSILValue(); + return {builder().createStructElementAddr(regularLoc(), v, v->getType().getFieldDecl(fieldIndex))}; + } + SWIFT_IMPORT_UNSAFE BridgedInstruction createTuple(swift::SILType type, BridgedValueArray elements) const { llvm::SmallVector elementValues; return {builder().createTuple(regularLoc(), type, elements.getValues(elementValues))}; } + SWIFT_IMPORT_UNSAFE + BridgedInstruction createTupleExtract(BridgedValue str, SwiftInt elementIndex) const { + swift::SILValue v = str.getSILValue(); + return {builder().createTupleExtract(regularLoc(), v, elementIndex)}; + } + + SWIFT_IMPORT_UNSAFE + BridgedInstruction createTupleElementAddr(BridgedValue addr, SwiftInt elementIndex) const { + swift::SILValue v = addr.getSILValue(); + return {builder().createTupleElementAddr(regularLoc(), v, elementIndex)}; + } + SWIFT_IMPORT_UNSAFE BridgedInstruction createStore(BridgedValue src, BridgedValue dst, SwiftInt ownership) const { diff --git a/include/swift/SIL/SILType.h b/include/swift/SIL/SILType.h index 7989350b96309..2d314a334ccbe 100644 --- a/include/swift/SIL/SILType.h +++ b/include/swift/SIL/SILType.h @@ -564,6 +564,8 @@ class SILType { bool isFunction() const { return is(); } bool isMetatype() const { return is(); } + VarDecl *getFieldDecl(intptr_t fieldIndex) const; + /// Given that this is a nominal type, return the lowered type of /// the given field. Applies substitutions as necessary. The /// result will be an address type if the base type is an address diff --git a/lib/SIL/IR/SILType.cpp b/lib/SIL/IR/SILType.cpp index a466145995b43..75a91a8f51995 100644 --- a/lib/SIL/IR/SILType.cpp +++ b/lib/SIL/IR/SILType.cpp @@ -320,6 +320,12 @@ static void addFieldSubstitutionsIfNeeded(TypeConverter &TC, SILType ty, } } +VarDecl *SILType::getFieldDecl(intptr_t fieldIndex) const { + NominalTypeDecl *decl = getNominalOrBoundGenericNominal(); + assert(decl && "expected nominal type"); + return getIndexedField(decl, fieldIndex); +} + SILType SILType::getFieldType(VarDecl *field, TypeConverter &TC, TypeExpansionContext context) const { AbstractionPattern origFieldTy = TC.getAbstractionPattern(field); @@ -361,9 +367,7 @@ SILType SILType::getFieldType(VarDecl *field, SILFunction *fn) const { } SILType SILType::getFieldType(intptr_t fieldIndex, SILFunction *function) const { - NominalTypeDecl *decl = getNominalOrBoundGenericNominal(); - assert(decl && "expected nominal type"); - VarDecl *field = getIndexedField(decl, fieldIndex); + VarDecl *field = getFieldDecl(fieldIndex); return getFieldType(field, function->getModule(), function->getTypeExpansionContext()); } From d635c89eb46a9302717f32ba265fb0b0e96e2439 Mon Sep 17 00:00:00 2001 From: Erik Eckstein Date: Wed, 5 Jul 2023 15:19:48 +0200 Subject: [PATCH 05/10] AccessUtils: add `AccessBase.isEqual` and `AccessPath.isEqualOrOverlaps` Also add `Value.referenceRoot` --- .../Optimizer/Utilities/AccessUtils.swift | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/SwiftCompilerSources/Sources/Optimizer/Utilities/AccessUtils.swift b/SwiftCompilerSources/Sources/Optimizer/Utilities/AccessUtils.swift index 4f35045cafde5..bfd3207a37633 100644 --- a/SwiftCompilerSources/Sources/Optimizer/Utilities/AccessUtils.swift +++ b/SwiftCompilerSources/Sources/Optimizer/Utilities/AccessUtils.swift @@ -162,6 +162,29 @@ enum AccessBase : CustomStringConvertible, Hashable { } } + // Returns true if it's guaranteed that this access has the same base address as the `other` access. + func isEqual(to other: AccessBase) -> Bool { + switch (self, other) { + case (.box(let pb1), .box(let pb2)): + return pb1.box.referenceRoot == pb2.box.referenceRoot + case (.class(let rea1), .class(let rea2)): + return rea1.fieldIndex == rea2.fieldIndex && + rea1.instance.referenceRoot == rea2.instance.referenceRoot + case (.stack(let as1), .stack(let as2)): + return as1 == as2 + case (.global(let gl1), .global(let gl2)): + return gl1 == gl2 + case (.argument(let arg1), .argument(let arg2)): + return arg1 == arg2 + case (.yield(let ba1), .yield(let ba2)): + return ba1 == ba2 + case (.pointer(let p1), .pointer(let p2)): + return p1 == p2 + default: + return false + } + } + /// Returns `true` if the two access bases do not alias. func isDistinct(from other: AccessBase) -> Bool { @@ -251,6 +274,13 @@ struct AccessPath : CustomStringConvertible { } return false } + + func isEqualOrOverlaps(_ other: AccessPath) -> Bool { + return base.isEqual(to: other.base) && + // Note: an access with a smaller path overlaps an access with larger path, e.g. + // `s0` overlaps `s0.s1` + projectionPath.isSubPath(of: other.projectionPath) + } } private func canBeOperandOfIndexAddr(_ value: Value) -> Bool { @@ -449,6 +479,25 @@ extension Value { } return .base(walker.result.base) } + + /// The root definition of a reference, obtained by skipping casts, etc. + var referenceRoot: Value { + var value: Value = self + while true { + switch value { + case is BeginBorrowInst, is CopyValueInst, is MoveValueInst, + is UpcastInst, is UncheckedRefCastInst, is EndCOWMutationInst: + value = (value as! Instruction).operands[0].value + case let mvr as MultipleValueInstructionResult: + guard let bcm = mvr.parentInstruction as? BeginCOWMutationInst else { + return value + } + value = bcm.instance + default: + return value + } + } + } } /// A ValueUseDef walker that that visits access storage paths of an address. From ff88efc1f6b80797cadea30d8b83fc28a7b29a8f Mon Sep 17 00:00:00 2001 From: Erik Eckstein Date: Wed, 5 Jul 2023 15:21:09 +0200 Subject: [PATCH 06/10] - walk utils --- SwiftCompilerSources/Sources/SIL/Instruction.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SwiftCompilerSources/Sources/SIL/Instruction.swift b/SwiftCompilerSources/Sources/SIL/Instruction.swift index 9bb9fd60374a1..f87b144e4e6ef 100644 --- a/SwiftCompilerSources/Sources/SIL/Instruction.swift +++ b/SwiftCompilerSources/Sources/SIL/Instruction.swift @@ -460,7 +460,9 @@ class InitExistentialRefInst : SingleValueInstruction, UnaryInstruction { } final public -class OpenExistentialRefInst : SingleValueInstruction, UnaryInstruction {} +class OpenExistentialRefInst : SingleValueInstruction, UnaryInstruction { + public var existential: Value { operand.value } +} final public class InitExistentialValueInst : SingleValueInstruction, UnaryInstruction {} From ab915c308d8a40eb72dbddd9a54b8ee01a7f96ea Mon Sep 17 00:00:00 2001 From: Erik Eckstein Date: Wed, 5 Jul 2023 15:22:15 +0200 Subject: [PATCH 07/10] Swift SIL: add the `Deallocation.allocatedValue` instruction API --- SwiftCompilerSources/Sources/SIL/Instruction.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/SwiftCompilerSources/Sources/SIL/Instruction.swift b/SwiftCompilerSources/Sources/SIL/Instruction.swift index f87b144e4e6ef..1792b24c1efd8 100644 --- a/SwiftCompilerSources/Sources/SIL/Instruction.swift +++ b/SwiftCompilerSources/Sources/SIL/Instruction.swift @@ -345,7 +345,14 @@ final public class UnimplementedRefCountingInst : RefCountingInst {} // no-value deallocation instructions //===----------------------------------------------------------------------===// -public protocol Deallocation : Instruction { } +public protocol Deallocation : Instruction { + var allocatedValue: Value { get } +} + +extension Deallocation { + public var allocatedValue: Value { operands[0].value } +} + final public class DeallocStackInst : Instruction, UnaryInstruction, Deallocation { public var allocstack: AllocStackInst { From 0494795ab4d9cf3833e2d944db515c29f5876d47 Mon Sep 17 00:00:00 2001 From: Erik Eckstein Date: Wed, 5 Jul 2023 15:22:36 +0200 Subject: [PATCH 08/10] Swift SIL: make BasicBlock Equatable --- SwiftCompilerSources/Sources/SIL/BasicBlock.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/SwiftCompilerSources/Sources/SIL/BasicBlock.swift b/SwiftCompilerSources/Sources/SIL/BasicBlock.swift index f5eb6d708cb7b..c9edcfdbe4b54 100644 --- a/SwiftCompilerSources/Sources/SIL/BasicBlock.swift +++ b/SwiftCompilerSources/Sources/SIL/BasicBlock.swift @@ -14,7 +14,7 @@ import Basic import SILBridging @_semantics("arc.immortal") -final public class BasicBlock : CustomStringConvertible, HasShortDescription { +final public class BasicBlock : CustomStringConvertible, HasShortDescription, Equatable { public var next: BasicBlock? { bridged.getNext().block } public var previous: BasicBlock? { bridged.getPrevious().block } @@ -65,12 +65,11 @@ final public class BasicBlock : CustomStringConvertible, HasShortDescription { public var name: String { "bb\(index)" } + public static func == (lhs: BasicBlock, rhs: BasicBlock) -> Bool { lhs === rhs } + public var bridged: BridgedBasicBlock { BridgedBasicBlock(SwiftObject(self)) } } -public func == (lhs: BasicBlock, rhs: BasicBlock) -> Bool { lhs === rhs } -public func != (lhs: BasicBlock, rhs: BasicBlock) -> Bool { lhs !== rhs } - /// The list of instructions in a BasicBlock. /// /// It's allowed to delete the current, next or any other instructions while From f96c13e9250a4e98aa206694f4b2b8cf971804dd Mon Sep 17 00:00:00 2001 From: Erik Eckstein Date: Wed, 5 Jul 2023 15:54:50 +0200 Subject: [PATCH 09/10] MemBehavior: correctly handle `debug_value` debug_value just "reads" the operand if it is an address. --- .../TestPasses/MemBehaviorDumper.swift | 3 ++- lib/SILOptimizer/Analysis/MemoryBehavior.cpp | 10 +++++++++ test/SILOptimizer/mem-behavior.sil | 22 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/SwiftCompilerSources/Sources/Optimizer/TestPasses/MemBehaviorDumper.swift b/SwiftCompilerSources/Sources/Optimizer/TestPasses/MemBehaviorDumper.swift index 21b6600a18ea3..4f1d74b835fac 100644 --- a/SwiftCompilerSources/Sources/Optimizer/TestPasses/MemBehaviorDumper.swift +++ b/SwiftCompilerSources/Sources/Optimizer/TestPasses/MemBehaviorDumper.swift @@ -71,7 +71,8 @@ private extension Instruction { is LoadInst, is StoreInst, is CopyAddrInst, - is BuiltinInst: + is BuiltinInst, + is DebugValueInst: return true default: return false diff --git a/lib/SILOptimizer/Analysis/MemoryBehavior.cpp b/lib/SILOptimizer/Analysis/MemoryBehavior.cpp index 15a3bca6bc61a..d3d9218af828c 100644 --- a/lib/SILOptimizer/Analysis/MemoryBehavior.cpp +++ b/lib/SILOptimizer/Analysis/MemoryBehavior.cpp @@ -176,6 +176,7 @@ class MemoryBehaviorVisitor MemBehavior visitDestroyValueInst(DestroyValueInst *DVI); MemBehavior visitSetDeallocatingInst(SetDeallocatingInst *BI); MemBehavior visitBeginCOWMutationInst(BeginCOWMutationInst *BCMI); + MemBehavior visitDebugValueInst(DebugValueInst *dv); #define ALWAYS_OR_SOMETIMES_LOADABLE_CHECKED_REF_STORAGE(Name, ...) \ MemBehavior visit##Name##ReleaseInst(Name##ReleaseInst *BI); #include "swift/AST/ReferenceStorage.def" @@ -374,6 +375,15 @@ visitBeginCOWMutationInst(BeginCOWMutationInst *BCMI) { return MemBehavior::None; } +MemBehavior MemoryBehaviorVisitor:: +visitDebugValueInst(DebugValueInst *dv) { + SILValue op = dv->getOperand(); + if (op->getType().isAddress() && mayAlias(op)) { + return MemBehavior::MayRead; + } + return MemBehavior::None; +} + //===----------------------------------------------------------------------===// // Top Level Entrypoint //===----------------------------------------------------------------------===// diff --git a/test/SILOptimizer/mem-behavior.sil b/test/SILOptimizer/mem-behavior.sil index 47d0582c2a57d..b7cbbff763dc7 100644 --- a/test/SILOptimizer/mem-behavior.sil +++ b/test/SILOptimizer/mem-behavior.sil @@ -1002,3 +1002,25 @@ bb0(%0 : $Builtin.RawPointer): %6 = tuple () return %6 : $() } + + +// CHECK-LABEL: @test_debug_value +// CHECK: PAIR #0. +// CHECK-NEXT: debug_value %0 : $*Int, let, name "x" +// CHECK-NEXT: %0 = alloc_stack $Int +// CHECK-NEXT: r=1,w=0 +// CHECK: PAIR #1. +// CHECK-NEXT: debug_value %0 : $*Int, let, name "x" +// CHECK-NEXT: %1 = alloc_stack $Int +// CHECK-NEXT: r=0,w=0 +sil @test_debug_value : $@convention(thin) () -> () { +bb0: + %0 = alloc_stack $Int + %1 = alloc_stack $Int + debug_value %0 : $*Int, let, name "x" + dealloc_stack %1 : $*Int + dealloc_stack %0 : $*Int + %r = tuple () + return %r : $() +} + From baaf5565b088a297935507d257e2b977b37ebbca Mon Sep 17 00:00:00 2001 From: Erik Eckstein Date: Wed, 5 Jul 2023 16:43:05 +0200 Subject: [PATCH 10/10] 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 --- .../Optimizer/FunctionPasses/CMakeLists.txt | 1 + .../FunctionPasses/DeadStoreElimination.swift | 267 ++++ .../PassManager/PassRegistration.swift | 1 + .../swift/SILOptimizer/PassManager/Passes.def | 2 +- lib/SILOptimizer/Transforms/CMakeLists.txt | 1 - .../Transforms/DeadStoreElimination.cpp | 1311 ----------------- test/SILOptimizer/dead_store_elim.sil | 73 +- ...ualize_protocol_composition_two_stores.sil | 2 + .../escape_analysis_dead_store.sil | 2 +- .../redundant_load_and_dead_store_elim.sil | 4 +- 10 files changed, 348 insertions(+), 1316 deletions(-) create mode 100644 SwiftCompilerSources/Sources/Optimizer/FunctionPasses/DeadStoreElimination.swift delete mode 100644 lib/SILOptimizer/Transforms/DeadStoreElimination.cpp diff --git a/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/CMakeLists.txt b/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/CMakeLists.txt index 1dfaa30884ae3..cc124655bf1c2 100644 --- a/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/CMakeLists.txt +++ b/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/CMakeLists.txt @@ -11,6 +11,7 @@ swift_compiler_sources(Optimizer CleanupDebugSteps.swift ComputeEscapeEffects.swift ComputeSideEffects.swift + DeadStoreElimination.swift InitializeStaticGlobals.swift ObjectOutliner.swift ObjCBridgingOptimization.swift diff --git a/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/DeadStoreElimination.swift b/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/DeadStoreElimination.swift new file mode 100644 index 0000000000000..c5e565743d478 --- /dev/null +++ b/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/DeadStoreElimination.swift @@ -0,0 +1,267 @@ +//===--- DeadStoreElimination.swift ----------------------------------------==// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 +// +//===----------------------------------------------------------------------===// + +import SIL + +/// Eliminates dead store instructions. +/// +/// A store is dead if after the store has occurred: +/// +/// 1. The value in memory is not read until the memory object is deallocated: +/// +/// %1 = alloc_stack +/// ... +/// store %2 to %1 +/// ... // no reads from %1 +/// dealloc_stack %1 +/// +/// 2. The value in memory is overwritten by another store before any potential read: +/// +/// store %2 to %1 +/// ... // no reads from %1 +/// store %3 to %1 +/// +/// In case of a partial dead store, the store is split so that some of the new +/// individual stores can be eliminated in the next round of the optimization: +/// +/// store %2 to %1 // partially dead +/// ... // no reads from %1 +/// %3 = struct_element_addr %1, #field1 +/// store %7 to %3 +/// -> +/// %3 = struct_extract %2, #field1 +/// %4 = struct_element_addr %1, #field1 +/// store %3 to %4 // this store is dead now +/// %5 = struct_extract %2, #field2 +/// %6 = struct_element_addr %1, #field2 +/// store %5 to %6 +/// ... // no reads from %1 +/// store %7 to %3 +/// +let deadStoreElimination = FunctionPass(name: "dead-store-elimination") { + (function: Function, context: FunctionPassContext) in + + for block in function.blocks { + + // We cannot use for-in iteration here because if the store is split, the new + // individual stores are inserted right afterwards and they would be ignored by a for-in iteration. + var inst = block.instructions.first + while let i = inst { + if let store = i as? StoreInst { + if !context.continueWithNextSubpassRun(for: store) { + return + } + tryEliminate(store: store, context) + } + inst = i.next + } + } +} + +private func tryEliminate(store: StoreInst, _ context: FunctionPassContext) { + if !store.hasValidOwnershipForDeadStoreElimination { + return + } + + switch store.isDead(context) { + case .alive: + break + case .dead: + context.erase(instruction: store) + case .maybePartiallyDead(let subPath): + // Check if the a partial store would really be dead to avoid unnecessary splitting. + switch store.isDead(at: subPath, context) { + case .alive, .maybePartiallyDead: + break + case .dead: + // The new individual stores are inserted right after the current store and + // will be optimized in the following loop iterations. + store.trySplit(context) + } + } +} + +private extension StoreInst { + + enum DataflowResult { + case alive + case dead + case maybePartiallyDead(AccessPath) + + init(aliveWith subPath: AccessPath?) { + if let subPath = subPath { + self = .maybePartiallyDead(subPath) + } else { + self = .alive + } + } + } + + func isDead( _ context: FunctionPassContext) -> DataflowResult { + return isDead(at: destination.accessPath, context) + } + + func isDead(at accessPath: AccessPath, _ context: FunctionPassContext) -> DataflowResult { + var worklist = InstructionWorklist(context) + defer { worklist.deinitialize() } + + worklist.pushIfNotVisited(self.next!) + + let storageDefBlock = accessPath.base.reference?.referenceRoot.parentBlock + var scanner = InstructionScanner(storePath: accessPath, storeAddress: self.destination, context.aliasAnalysis) + + while let startInstInBlock = worklist.pop() { + let block = startInstInBlock.parentBlock + switch scanner.scan(instructions: InstructionList(first: startInstInBlock)) { + case .transparent: + // Abort if we find the storage definition of the access in case of a loop, e.g. + // + // bb1: + // %storage_root = apply + // %2 = ref_element_addr %storage_root + // store %3 to %2 + // cond_br %c, bb1, bb2 + // + // The storage root is different in each loop iteration. Therefore the store of a + // successive loop iteration does not overwrite the store of the previous iteration. + if let storageDefBlock = storageDefBlock, + block.successors.contains(storageDefBlock) { + return DataflowResult(aliveWith: scanner.potentiallyDeadSubpath) + } + worklist.pushIfNotVisited(contentsOf: block.successors.lazy.map { $0.instructions.first! }) + case .dead: + break + case .alive: + return DataflowResult(aliveWith: scanner.potentiallyDeadSubpath) + } + } + return .dead + } + + func trySplit(_ context: FunctionPassContext) { + let type = source.type + if type.isStruct { + let builder = Builder(after: self, context) + for idx in 0.. Result { + for inst in instructions { + switch inst { + case let successiveStore as StoreInst: + let successivePath = successiveStore.destination.accessPath + if successivePath.isEqualOrOverlaps(storePath) { + return .dead + } + if storePath.isEqualOrOverlaps(successivePath), + potentiallyDeadSubpath == nil { + // Storing to a sub-field of the original store doesn't make the original store dead. + // But when we split the original store, then one of the new individual stores might be + // overwritten by this store. + potentiallyDeadSubpath = successivePath + } + case is DeallocRefInst, is DeallocStackRefInst, is DeallocBoxInst: + if (inst as! Deallocation).isDeallocation(of: storePath.base) { + return .dead + } + case let ds as DeallocStackInst: + if ds.isStackDeallocation(of: storePath.base) { + return .dead + } + case is FixLifetimeInst, is EndAccessInst: + break + case let term as TermInst: + if term.isFunctionExiting { + return .alive + } + fallthrough + default: + budget -= 1 + if budget == 0 { + return .alive + } + if inst.mayRead(fromAddress: storeAddress, aliasAnalysis) { + return .alive + } + } + } + return .transparent + } +} + +private extension Deallocation { + func isDeallocation(of base: AccessBase) -> Bool { + if let accessReference = base.reference, + accessReference.referenceRoot == self.allocatedValue.referenceRoot { + return true + } + return false + } +} + +private extension DeallocStackInst { + func isStackDeallocation(of base: AccessBase) -> Bool { + if case .stack(let allocStack) = base, allocstack == allocStack { + return true + } + return false + } +} diff --git a/SwiftCompilerSources/Sources/Optimizer/PassManager/PassRegistration.swift b/SwiftCompilerSources/Sources/Optimizer/PassManager/PassRegistration.swift index ac83d8ede2a78..95a5a04bf8849 100644 --- a/SwiftCompilerSources/Sources/Optimizer/PassManager/PassRegistration.swift +++ b/SwiftCompilerSources/Sources/Optimizer/PassManager/PassRegistration.swift @@ -82,6 +82,7 @@ private func registerSwiftPasses() { registerPass(cleanupDebugStepsPass, { cleanupDebugStepsPass.run($0) }) registerPass(namedReturnValueOptimization, { namedReturnValueOptimization.run($0) }) registerPass(stripObjectHeadersPass, { stripObjectHeadersPass.run($0) }) + registerPass(deadStoreElimination, { deadStoreElimination.run($0) }) // Instruction passes registerForSILCombine(BeginCOWMutationInst.self, { run(BeginCOWMutationInst.self, $0) }) diff --git a/include/swift/SILOptimizer/PassManager/Passes.def b/include/swift/SILOptimizer/PassManager/Passes.def index efca539f5cd0a..149edb0e71424 100644 --- a/include/swift/SILOptimizer/PassManager/Passes.def +++ b/include/swift/SILOptimizer/PassManager/Passes.def @@ -243,7 +243,7 @@ PASS(EarlyRedundantLoadElimination, "early-redundant-load-elim", "Early Redundant Load Elimination") PASS(RedundantLoadElimination, "redundant-load-elim", "Redundant Load Elimination") -PASS(DeadStoreElimination, "dead-store-elim", +SWIFT_FUNCTION_PASS(DeadStoreElimination, "dead-store-elimination", "Dead Store Elimination") PASS(GenericSpecializer, "generic-specializer", "Generic Function Specialization on Static Types") diff --git a/lib/SILOptimizer/Transforms/CMakeLists.txt b/lib/SILOptimizer/Transforms/CMakeLists.txt index 7e4bd1c81f009..8c60439433892 100644 --- a/lib/SILOptimizer/Transforms/CMakeLists.txt +++ b/lib/SILOptimizer/Transforms/CMakeLists.txt @@ -14,7 +14,6 @@ target_sources(swiftSILOptimizer PRIVATE CopyPropagation.cpp DeadCodeElimination.cpp DeadObjectElimination.cpp - DeadStoreElimination.cpp DestroyHoisting.cpp Devirtualizer.cpp DifferentiabilityWitnessDevirtualizer.cpp diff --git a/lib/SILOptimizer/Transforms/DeadStoreElimination.cpp b/lib/SILOptimizer/Transforms/DeadStoreElimination.cpp deleted file mode 100644 index 0398693598348..0000000000000 --- a/lib/SILOptimizer/Transforms/DeadStoreElimination.cpp +++ /dev/null @@ -1,1311 +0,0 @@ -//===--- DeadStoreElimination.cpp - SIL Dead Store Elimination ------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2017 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 -// -//===----------------------------------------------------------------------===// -/// -/// \file -/// -/// This pass eliminates dead stores across basic blocks. -/// -/// A store is dead if after the store has occurred: -/// -/// 1. The store to pointer is not used along any path to program exit. -/// 2. The store to pointer is overwritten by another store before any -/// potential use of the pointer. -/// -/// Dead store elimination (DSE) eliminates such stores by: -/// -/// 1. Introducing a notion of a LSLocation that is used to model objects -/// fields. (See below for more details). -/// -/// 2. Performing a post-order walk over the control flow graph, tracking any -/// LSLocations that are read from or stored into in each basic block. After -/// eliminating any dead stores in single blocks, it computes a genset and -/// killset for each block. The genset keeps a list of upward visible stores -/// and the killset keeps a list of LSLocation this basic block reads (kills). -/// -/// 3. An optimistic iterative dataflow is performed on the genset and killset -/// until convergence. -/// -/// At the core of DSE, there is the LSLocation class. a LSLocation is an -/// abstraction of an object field in program. It consists of a base and a -/// projection path to the field accessed. -/// -/// When a load or store instruction is encountered, the memory is broken down -/// to the indivisible components, i.e aggregates are broken down to their -/// individual fields using the expand function. This gives the flexibility to -/// find exactly which part of the store is alive and which part is dead. -/// -/// After the live parts of the store are determined, they are merged into the -/// minimum number of stores possible using the reduce function. This is done -/// so that we do not bloat SIL IR. -/// -/// Another way to implement the DSE optimization is to keep the instructions -/// that read and/or write memory without breaking the memory read/written -/// using the ProjectionPath. However, this can easily lead to loss of -/// opportunities, e.g. a read that only kills part of a store may need to be -/// treated as killing the entire store. However, using ProjectionPath does -/// lead to more memory uses. -/// -//===----------------------------------------------------------------------===// - -#define DEBUG_TYPE "sil-dead-store-elim" -#include "swift/SIL/Projection.h" -#include "swift/SIL/SILArgument.h" -#include "swift/SIL/SILBuilder.h" -#include "swift/SIL/MemoryLocations.h" -#include "swift/SILOptimizer/Analysis/AliasAnalysis.h" -#include "swift/SILOptimizer/Analysis/PostOrderAnalysis.h" -#include "swift/SILOptimizer/PassManager/Passes.h" -#include "swift/SILOptimizer/PassManager/Transforms.h" -#include "swift/SILOptimizer/Utils/CFGOptUtils.h" -#include "swift/SILOptimizer/Utils/InstOptUtils.h" -#include "swift/SILOptimizer/Utils/LoadStoreOptUtils.h" -#include "swift/SIL/BasicBlockData.h" -#include "swift/SIL/BasicBlockDatastructures.h" -#include "llvm/ADT/BitVector.h" -#include "llvm/ADT/DenseSet.h" -#include "llvm/ADT/Statistic.h" -#include "llvm/Support/Allocator.h" -#include "llvm/Support/CommandLine.h" -#include "llvm/Support/Debug.h" - -using namespace swift; - -STATISTIC(NumDeadStores, "Number of dead stores removed"); -STATISTIC(NumPartialDeadStores, "Number of partial dead stores removed"); - -/// If a large store is broken down to too many smaller stores, bail out. -/// Currently, we only do partial dead store if we can form a single contiguous -/// live store. -static llvm::cl::opt -MaxPartialStoreCount("max-partial-store-count", llvm::cl::init(1), llvm::cl::Hidden); - -/// ComputeMaxStoreSet - If we ignore all reads, what is the max store set that -/// can reach a particular point in a basic block. This helps in generating -/// the genset and killset. i.e. if there is no upward visible store that can -/// reach the beginning of a basic block, then we know that the genset and -/// killset for the stored location need not be set for the basic block. -/// -/// BuildGenKillSet - Build the genset and killset of the basic block. -/// -/// PerformDSE - Perform the actual dead store elimination. -enum class DSEKind : unsigned { - ComputeMaxStoreSet = 0, - BuildGenKillSet = 1, - PerformDSE = 2, -}; - -//===----------------------------------------------------------------------===// -// Utility Functions -//===----------------------------------------------------------------------===// - -/// Return the deallocate stack instructions corresponding to the given -/// AllocStackInst. -static llvm::SmallVector -findDeallocStackInst(AllocStackInst *ASI) { - llvm::SmallVector DSIs; - for (auto UI = ASI->use_begin(), E = ASI->use_end(); UI != E; ++UI) { - if (auto *D = dyn_cast(UI->getUser())) { - DSIs.push_back(D); - } - } - return DSIs; -} - -/// Return the deallocate ref instructions corresponding to the given -/// AllocRefInst. -static llvm::SmallVector -findDeallocStackRefInst(AllocRefInstBase *ARI) { - llvm::SmallVector DSIs; - for (auto UI = ARI->use_begin(), E = ARI->use_end(); UI != E; ++UI) { - if (auto *D = dyn_cast(UI->getUser())) - DSIs.push_back(D); - } - return DSIs; -} - -static inline bool isComputeMaxStoreSet(DSEKind Kind) { - return Kind == DSEKind::ComputeMaxStoreSet; -} - -static inline bool isBuildingGenKillSet(DSEKind Kind) { - return Kind == DSEKind::BuildGenKillSet; -} - -static inline bool isPerformingDSE(DSEKind Kind) { - return Kind == DSEKind::PerformDSE; -} - -/// Returns true if this is an instruction that may have side effects in a -/// general sense but are inert from a load store perspective. -static bool isDeadStoreInertInstruction(SILInstruction *Inst) { - switch (Inst->getKind()) { -#define UNCHECKED_REF_STORAGE(Name, ...) \ - case SILInstructionKind::StrongCopy##Name##ValueInst: -#define ALWAYS_OR_SOMETIMES_LOADABLE_CHECKED_REF_STORAGE(Name, ...) \ - case SILInstructionKind::Name##RetainInst: \ - case SILInstructionKind::StrongRetain##Name##Inst: \ - case SILInstructionKind::StrongCopy##Name##ValueInst: -#include "swift/AST/ReferenceStorage.def" - case SILInstructionKind::StrongRetainInst: - case SILInstructionKind::RetainValueInst: - case SILInstructionKind::DeallocStackInst: - case SILInstructionKind::DeallocStackRefInst: - case SILInstructionKind::DeallocRefInst: - case SILInstructionKind::CondFailInst: - case SILInstructionKind::FixLifetimeInst: - case SILInstructionKind::EndAccessInst: - case SILInstructionKind::SetDeallocatingInst: - return true; - default: - return false; - } -} - -//===----------------------------------------------------------------------===// -// Basic Block Location State -//===----------------------------------------------------------------------===// - -namespace { -/// If this function has too many basic blocks or too many locations, it may -/// take a long time to compute the genset and killset. The number of memory -/// behavior or alias query we need to do in worst case is roughly linear to -/// # of BBs x(times) # of locations. -/// -/// we could run DSE on functions with 256 basic blocks and 256 locations, -/// which is a large function. -constexpr unsigned MaxLSLocationBBMultiplicationNone = 256*256; - -/// we could run optimistic DSE on functions with less than 64 basic blocks -/// and 64 locations which is a sizable function. -constexpr unsigned MaxLSLocationBBMultiplicationPessimistic = 64*64; - -/// forward declaration. -class DSEContext; -/// BlockState summarizes how LSLocations are used in a basic block. -/// -/// Initially the BBWriteSetOut is empty. Before a basic block is processed, it -/// is initialized to the intersection of BBWriteSetIns of all successors of the -/// basic block. -/// -/// BBWriteSetMid is initialized to BBWriteSetOut of the current basic block -/// before instructions in the basic block is processed. -/// -/// Initially BBWriteSetIn is set to true. After the basic block is processed, -/// if its BBWriteSetMid is different from BBWriteSetIn, BBWriteSetIn is -/// assigned the value of BBWriteSetMid and the data flow is rerun on the -/// current basic block's predecessors. -/// -/// Instructions in each basic block are processed in post-order as follows: -/// -/// 1. When a store instruction is encountered, the stored location is tracked. -/// -/// 2. When a load instruction is encountered, remove the loaded location and -/// any location it may alias with from the BBWriteSetMid. -/// -/// 3. When an instruction reads from memory in an unknown way, the BBWriteSet -/// bit is cleared if the instruction can read the corresponding LSLocation. -class BlockState { -public: - /// The basic block this BlockState represents. - SILBasicBlock *BB = nullptr; - - /// Keep the number of LSLocations in the LocationVault. - unsigned LocationNum = 0; - - /// A bit vector for which the ith bit represents the ith LSLocation in - /// LocationVault. If the bit is set, then the location currently has an - /// upward visible store at the end of the basic block. - SmallBitVector BBWriteSetOut; - - /// A bit vector for which the ith bit represents the ith LSLocation in - /// LocationVault. If the bit is set, then the location currently has an - /// upward visible store in middle of the basic block. - SmallBitVector BBWriteSetMid; - - /// A bit vector for which the ith bit represents the ith LSLocation in - /// LocationVault. If a bit in the vector is set, then the location has an - /// upward visible store at the beginning of the basic block. - SmallBitVector BBWriteSetIn; - - /// A bit vector for which the ith bit represents the ith LSLocation in - /// LocationVault. If the bit is set, then the current basic block - /// generates an upward visible store. - SmallBitVector BBGenSet; - - /// A bit vector for which the ith bit represents the ith LSLocation in - /// LocationVault. If the bit is set, then the current basic block - /// kills an upward visible store. - SmallBitVector BBKillSet; - - /// A bit vector to keep the maximum number of stores that can reach a - /// certain point of the basic block. If a bit is set, that means there is - /// potentially an upward visible store to the location at the particular - /// point of the basic block. - SmallBitVector BBMaxStoreSet; - - /// If a bit in the vector is set, then the location is dead at the end of - /// this basic block. - SmallBitVector BBDeallocateLocation; - - /// The dead stores in the current basic block. - llvm::SmallVector DeadStores; - - /// Keeps track of what stores to generate after the data flow stabilizes. - /// these stores come from partial dead stores. - /// - /// The first SILValue keeps the address of the live store and the second - /// SILValue keeps the value of the store. - llvm::SetVector LiveAddr; - llvm::DenseMap LiveStores; - - /// Constructors. - BlockState() = default; - - void dump(); - - /// Initialize the bitvectors for the current basic block. - void init(SILBasicBlock *BB, unsigned LocationNum, bool Optimistic); - - /// Check whether the BBWriteSetIn has changed. If it does, we need to rerun - /// the data flow on this block's predecessors to reach fixed point. - bool updateBBWriteSetIn(SmallBitVector &X); - - /// Functions to manipulate the write set. - void startTrackingLocation(SmallBitVector &BV, unsigned bit); - void stopTrackingLocation(SmallBitVector &BV, unsigned bit); - bool isTrackingLocation(SmallBitVector &BV, unsigned bit); - - /// Set the store bit for stack slot deallocated in this basic block. - void initStoreSetAtEndOfBlock(DSEContext &Ctx); -}; - -} // end anonymous namespace - -bool BlockState::updateBBWriteSetIn(SmallBitVector &X) { - if (BBWriteSetIn == X) - return false; - BBWriteSetIn = X; - return true; -} - -void BlockState::startTrackingLocation(SmallBitVector &BV, unsigned i) { - BV.set(i); -} - -void BlockState::stopTrackingLocation(SmallBitVector &BV, unsigned i) { - BV.reset(i); -} - -bool BlockState::isTrackingLocation(SmallBitVector &BV, unsigned i) { - return BV.test(i); -} - -//===----------------------------------------------------------------------===// -// Top Level Implementation -//===----------------------------------------------------------------------===// - -namespace { -/// The dead store elimination context, keep information about stores in a basic -/// block granularity. -class DSEContext { -/// How to process the current function. -enum class ProcessKind { - ProcessOptimistic = 0, - ProcessPessimistic = 1, - ProcessNone = 2, -}; - -private: - /// The module we are currently processing. - SILModule *Mod; - - /// The function we are currently processing. - SILFunction *F; - - /// Pass manager, used to get various analysis. - SILPassManager *PM; - - /// Alias Analysis. - AliasAnalysis *AA; - - /// Type Expansion Analysis. - TypeExpansionAnalysis *TE; - - /// Epilogue release analysis. - EpilogueARCFunctionInfo *EAFI; - - /// Map every basic block to its location state. - BasicBlockData BBToLocState; - - /// Keeps all the locations for the current function. The BitVector in each - /// BlockState is then laid on top of it to keep track of which LSLocation - /// has an upward visible store. - std::vector LocationVault; - - /// Keeps a list of basic blocks that have StoreInsts. If a basic block does - /// not have StoreInst, we do not actually perform the last iteration where - /// DSE is actually performed on the basic block. - /// - /// NOTE: This is never populated for functions which will only require 1 - /// data flow iteration. For function that requires more than 1 iteration of - /// the data flow this is populated when the first time the functions is - /// walked, i.e. when the we generate the genset and killset. - BasicBlockSet BBWithStores; - - /// Contains a map between location to their index in the LocationVault. - /// used to facilitate fast location to index lookup. - LSLocationIndexMap LocToBitIndex; - - /// Keeps a map between the accessed SILValue and the location. - LSLocationBaseMap BaseToLocIndex; - - /// Return the BlockState for the basic block this basic block belongs to. - BlockState *getBlockState(SILBasicBlock *B) { return &BBToLocState[B]; } - - /// Return the BlockState for the basic block this instruction belongs to. - BlockState *getBlockState(SILInstruction *I) { - return getBlockState(I->getParent()); - } - - /// LSLocation written has been extracted, expanded and mapped to the bit - /// position in the bitvector. update the max store set using the bit - /// position. - void processWriteForMaxStoreSet(BlockState *S, unsigned bit); - - /// There is a read to a location, expand the location into individual fields - /// before processing them. - void processRead(SILInstruction *Inst, SILValue M, DSEKind K); - void processReadForGenKillSet(BlockState *S, unsigned bit); - void processReadForDSE(BlockState *S, unsigned Bit); - - /// There is a write to a location, expand the location into individual fields - /// before processing them. - void processWrite(SILInstruction *Inst, SILValue V, SILValue M, DSEKind K); - void processWriteForGenKillSet(BlockState *S, unsigned bit); - bool processWriteForDSE(BlockState *S, unsigned bit); - - /// Process instructions. Extract locations from SIL LoadInst. - void processLoadInst(SILInstruction *Inst, DSEKind Kind); - - /// Process instructions. Extract locations from SIL StoreInst. - void processStoreInst(SILInstruction *Inst, DSEKind Kind); - - /// Process instructions. Extract locations from SIL DebugValueInst. - /// DebugValueInst w/ address value maybe promoted to DebugValueInst w/ - /// scalar value when this is done, - /// DebugValueInst is effectively a read on the location. - void processDebugValueAddrInst(SILInstruction *I, DSEKind Kind); - void processDebugValueAddrInstForGenKillSet(SILInstruction *I); - void processDebugValueAddrInstForDSE(SILInstruction *I); - - /// Process unknown read instructions. Extract locations from unknown memory - /// inst. - void processUnknownReadInst(SILInstruction *Inst, DSEKind Kind); - void processUnknownReadInstForGenKillSet(SILInstruction *Inst); - void processUnknownReadInstForDSE(SILInstruction *Inst); - - /// Check whether the instruction invalidate any locations due to change in - /// its location Base. - /// - /// This is to handle a case like this. - /// - /// class Foo { var a : Int = 12 } - /// for _ in 0 ...x { - /// x = Foo(); - /// x.a = 13 - /// } - /// x.a = 12 - /// - /// In this case, DSE cannot remove the x.a = 13 inside the loop. - /// - /// To do this, when the algorithm reaches the beginning of the basic block in - /// the loop it will need to invalidate the location in the BBWriteSetMid. - /// i.e. the base of the location is changed. - /// - /// If not, on the second iteration, the intersection of the successors of - /// the loop basic block will have store to x.a and therefore x.a = 13 can now - /// be considered dead. - void invalidateBase(SILValue B, BlockState *S, DSEKind Kind); - void invalidateBaseForGenKillSet(SILValue B, BlockState *S); - void invalidateBaseForDSE(SILValue B, BlockState *S); - - /// Get the bit representing the location in the LocationVault. - unsigned getLocationBit(const LSLocation &L); - -public: - /// Constructor. - DSEContext(SILFunction *F, SILModule *M, SILPassManager *PM, - AliasAnalysis *AA, TypeExpansionAnalysis *TE, - EpilogueARCFunctionInfo *EAFI, - llvm::SpecificBumpPtrAllocator &BPA) - : Mod(M), F(F), PM(PM), AA(AA), TE(TE), EAFI(EAFI), BBToLocState(F), - BBWithStores(F) {} - - void dump(); - - /// Entry point for dead store elimination. - bool run(); - - /// Run the iterative DF to converge the BBWriteSetIn. - void runIterativeDSE(); - - /// Returns the location vault of the current function. - std::vector &getLocationVault() { return LocationVault; } - - /// Use a set of ad hoc rules to tell whether we should run a pessimistic - /// one iteration data flow on the function. - ProcessKind getProcessFunctionKind(unsigned StoreCount); - - /// Compute the kill set for the basic block. return true if the store set - /// changes. - void processBasicBlockForDSE(SILBasicBlock *BB, bool Optimistic); - - /// Compute the genset and killset for the current basic block. - void processBasicBlockForGenKillSet(SILBasicBlock *BB); - - /// Compute the BBWriteSetOut and BBWriteSetIn for the current basic - /// block with the generated gen and kill set. - bool processBasicBlockWithGenKillSet(SILBasicBlock *BB); - - /// Intersect the successors' BBWriteSetIns. - void mergeSuccessorLiveIns(SILBasicBlock *BB); - - /// Update the BlockState based on the given instruction. - void processInstruction(SILInstruction *I, DSEKind Kind); -}; - -} // end anonymous namespace - -void BlockState::dump() { - llvm::dbgs() << " block " << BB->getDebugID() << ": in=" << BBWriteSetIn - << ", out=" << BBWriteSetOut << ", mid=" << BBWriteSetMid - << ", gen=" << BBGenSet << ", kill=" << BBKillSet << '\n'; -} - -void BlockState::init(SILBasicBlock *BB, unsigned LocationNum, bool Optimistic) { - this->BB = BB; - this->LocationNum = LocationNum; - - // For function that requires just 1 iteration of the data flow to converge - // we set the initial state of BBWriteSetIn to 0. - // - // For other functions, the initial state of BBWriteSetIn should be all 1's. - // Otherwise the dataflow solution could be too conservative. - // - // Consider this case, the dead store by var a = 10 before the loop will not - // be eliminated if the BBWriteSetIn is set to 0 initially. - // - // var a = 10 - // for _ in 0...1024 {} - // a = 10 - // - // However, by doing so, we can only eliminate the dead stores after the - // data flow stabilizes. - // - BBWriteSetIn.resize(LocationNum, Optimistic); - BBWriteSetOut.resize(LocationNum, false); - BBWriteSetMid.resize(LocationNum, false); - - // GenSet and KillSet initially empty. - BBGenSet.resize(LocationNum, false); - BBKillSet.resize(LocationNum, false); - - // MaxStoreSet is optimistically set to true initially. - BBMaxStoreSet.resize(LocationNum, true); - - // DeallocateLocation initially empty. - BBDeallocateLocation.resize(LocationNum, false); -} - -#if __has_attribute(used) -__attribute((used)) -#endif -void DSEContext::dump() { - llvm::dbgs() << "Locations:\n"; - unsigned idx = 0; - for (const LSLocation &loc : LocationVault) { - llvm::dbgs() << " #" << idx << ": " << loc.getBase(); - ++idx; - } - for (SILBasicBlock &BB : *F) { - getBlockState(&BB)->dump(); - } -} - -unsigned DSEContext::getLocationBit(const LSLocation &Loc) { - // Return the bit position of the given Loc in the LocationVault. The bit - // position is then used to set/reset the bitvector kept by each BlockState. - // - // We should have the location populated by the enumerateLSLocation at this - // point. - auto Iter = LocToBitIndex.find(Loc); - assert(Iter != LocToBitIndex.end() && "LSLocation should have been enum'ed"); - return Iter->second; -} - -DSEContext::ProcessKind DSEContext::getProcessFunctionKind(unsigned StoreCount) { - // Don't optimize function that are marked as 'no.optimize'. - if (!F->shouldOptimize()) - return ProcessKind::ProcessNone; - - // Really no point optimizing here as there is no dead stores. - if (StoreCount < 1) - return ProcessKind::ProcessNone; - - bool RunOneIteration = true; - unsigned BBCount = 0; - unsigned LocationCount = LocationVault.size(); - - // If all basic blocks will have their successors processed if - // the basic blocks in the functions are iterated in post order. - // Then this function can be processed in one iteration, i.e. no - // need to generate the genset and killset. - auto *PO = PM->getAnalysis()->get(F); - BasicBlockSet HandledBBs(F); - for (SILBasicBlock *B : PO->getPostOrder()) { - ++BBCount; - for (SILBasicBlock *succ : B->getSuccessors()) { - if (!HandledBBs.contains(succ)) { - RunOneIteration = false; - break; - } - } - HandledBBs.insert(B); - } - - // Data flow may take too long to run. - if (BBCount * LocationCount > MaxLSLocationBBMultiplicationNone) - return ProcessKind::ProcessNone; - - // This function's data flow would converge in 1 iteration. - if (RunOneIteration) - return ProcessKind::ProcessPessimistic; - - // We run one pessimistic data flow to do dead store elimination on - // the function. - if (BBCount * LocationCount > MaxLSLocationBBMultiplicationPessimistic) - return ProcessKind::ProcessPessimistic; - - return ProcessKind::ProcessOptimistic; -} - -void DSEContext::processBasicBlockForGenKillSet(SILBasicBlock *BB) { - // Compute the MaxStoreSet at the end of the basic block. - auto *BBState = getBlockState(BB); - if (BB->succ_empty()) { - BBState->BBMaxStoreSet |= BBState->BBDeallocateLocation; - } else { - auto Iter = BB->succ_begin(); - BBState->BBMaxStoreSet = getBlockState(*Iter)->BBMaxStoreSet; - Iter = std::next(Iter); - for (auto EndIter = BB->succ_end(); Iter != EndIter; ++Iter) { - BBState->BBMaxStoreSet &= getBlockState(*Iter)->BBMaxStoreSet; - } - } - - // Compute the genset and killset. - // - // Also compute the MaxStoreSet at the current position of the basic block. - // - // This helps generating the genset and killset. If there is no way a - // location can have an upward visible store at a particular point in the - // basic block, we do not need to turn on the genset and killset for the - // location. - // - // Turning on the genset and killset can be costly as it involves querying - // the AA interface. - for (auto I = BB->rbegin(), E = BB->rend(); I != E; ++I) { - // Only process store insts. - if (isa(*I)) { - if (!BBWithStores.contains(BB)) - BBWithStores.insert(BB); - processStoreInst(&(*I), DSEKind::ComputeMaxStoreSet); - } - - // Compute the genset and killset for this instruction. - processInstruction(&(*I), DSEKind::BuildGenKillSet); - } - - // Handle SILArgument for base invalidation. - ArrayRef Args = BB->getArguments(); - for (auto &X : Args) { - invalidateBase(X, BBState, DSEKind::BuildGenKillSet); - } -} - -bool DSEContext::processBasicBlockWithGenKillSet(SILBasicBlock *BB) { - // Compute the BBWriteSetOut at the end of the basic block. - mergeSuccessorLiveIns(BB); - - // Compute the BBWriteSet at the beginning of the basic block. - BlockState *S = getBlockState(BB); - S->BBWriteSetMid = S->BBWriteSetOut; - S->BBWriteSetMid.reset(S->BBKillSet); - S->BBWriteSetMid |= S->BBGenSet; - - // If BBWriteSetIn changes, then keep iterating until reached a fixed point. - return S->updateBBWriteSetIn(S->BBWriteSetMid); -} - -void DSEContext::processBasicBlockForDSE(SILBasicBlock *BB, bool Optimistic) { - // If we know this is not a one iteration function which means its - // its BBWriteSetIn and BBWriteSetOut have been computed and converged, - // and this basic block does not even have StoreInsts, there is no point - // in processing every instruction in the basic block again as no store - // will be eliminated. - if (Optimistic && !BBWithStores.contains(BB)) - return; - - // Intersect in the successor WriteSetIns. A store is dead if it is not read - // from any path to the end of the program. Thus an intersection. - mergeSuccessorLiveIns(BB); - - // Initialize the BBWriteSetMid to BBWriteSetOut to get started. - BlockState *S = getBlockState(BB); - S->BBWriteSetMid = S->BBWriteSetOut; - - // Process instructions in post-order fashion. - for (auto I = BB->rbegin(), E = BB->rend(); I != E; ++I) { - processInstruction(&(*I), DSEKind::PerformDSE); - } - - // Handle SILArgument for base invalidation. - ArrayRef Args = BB->getArguments(); - for (auto &X : Args) { - invalidateBase(X, S, DSEKind::BuildGenKillSet); - } - - S->BBWriteSetIn = S->BBWriteSetMid; -} - -void BlockState::initStoreSetAtEndOfBlock(DSEContext &Ctx) { - std::vector &LocationVault = Ctx.getLocationVault(); - // We set the store bit at the end of the basic block in which a stack - // allocated location is deallocated. - for (unsigned i = 0; i < LocationVault.size(); ++i) { - // Turn on the store bit at the block which the stack slot is deallocated. - if (auto *ASI = dyn_cast(LocationVault[i].getBase())) { - for (auto X : findDeallocStackInst(ASI)) { - SILBasicBlock *DSIBB = X->getParent(); - if (DSIBB != BB) - continue; - startTrackingLocation(BBDeallocateLocation, i); - } - } - if (auto *ARI = dyn_cast(LocationVault[i].getBase())) { - if (!ARI->isAllocatingStack()) - continue; - for (auto X : findDeallocStackRefInst(ARI)) { - SILBasicBlock *DSIBB = X->getParent(); - if (DSIBB != BB) - continue; - startTrackingLocation(BBDeallocateLocation, i); - } - } - } -} - -void DSEContext::mergeSuccessorLiveIns(SILBasicBlock *BB) { - // If basic block has no successor, then all local writes can be considered - // dead for block with no successor. - BlockState *C = getBlockState(BB); - if (BB->succ_empty()) { - if (isa(BB->getTerminator())) { - C->BBWriteSetOut.set(); - return; - } - C->BBWriteSetOut |= C->BBDeallocateLocation; - return; - } - - // Use the first successor as the base condition. - auto Iter = BB->succ_begin(); - C->BBWriteSetOut = getBlockState(*Iter)->BBWriteSetIn; - - /// Merge/intersection is very frequently performed, so it is important to - /// make it as cheap as possible. - /// - /// To do so, we canonicalize LSLocations, i.e. traced back to the underlying - /// object. Therefore, no need to do a O(N^2) comparison to figure out what is - /// dead along all successors. - /// - /// NOTE: Canonicalization does not solve the problem entirely. i.e. it is - /// still possible that 2 LSLocations with different bases that happen to be - /// the same object and field. In such case, we would miss a dead store - /// opportunity. But this happens less often with canonicalization. - Iter = std::next(Iter); - for (auto EndIter = BB->succ_end(); Iter != EndIter; ++Iter) { - C->BBWriteSetOut &= getBlockState(*Iter)->BBWriteSetIn; - } - - // We set the store bit at the end of the basic block in which a stack - // allocated location is deallocated. - C->BBWriteSetOut |= C->BBDeallocateLocation; -} - -void DSEContext::invalidateBaseForGenKillSet(SILValue B, BlockState *S) { - for (unsigned i = 0; i < S->LocationNum; ++i) { - if (LocationVault[i].getBase() != B) - continue; - S->startTrackingLocation(S->BBKillSet, i); - S->stopTrackingLocation(S->BBGenSet, i); - } -} - -void DSEContext::invalidateBaseForDSE(SILValue B, BlockState *S) { - for (unsigned i = 0; i < S->LocationNum; ++i) { - if (!S->BBWriteSetMid.test(i)) - continue; - if (LocationVault[i].getBase() != B) - continue; - S->stopTrackingLocation(S->BBWriteSetMid, i); - } -} - -void DSEContext::invalidateBase(SILValue B, BlockState *S, DSEKind Kind) { - // If this instruction defines the base of a location, then we need to - // invalidate any locations with the same base. - // - // Are we building genset and killset. - if (isBuildingGenKillSet(Kind)) { - invalidateBaseForGenKillSet(B, S); - return; - } - - // Are we performing dead store elimination. - if (isPerformingDSE(Kind)) { - invalidateBaseForDSE(B, S); - return; - } - - llvm_unreachable("Unknown DSE compute kind"); -} - -void DSEContext::processReadForDSE(BlockState *S, unsigned bit) { - // Remove any may/must-aliasing stores to the LSLocation, as they can't be - // used to kill any upward visible stores due to the interfering load. - LSLocation &R = LocationVault[bit]; - for (unsigned i = 0; i < S->LocationNum; ++i) { - if (!S->isTrackingLocation(S->BBWriteSetMid, i)) - continue; - LSLocation &L = LocationVault[i]; - if (!L.isMayAliasLSLocation(R, AA)) - continue; - S->stopTrackingLocation(S->BBWriteSetMid, i); - } -} - -void DSEContext::processReadForGenKillSet(BlockState *S, unsigned bit) { - // Start tracking the read to this LSLocation in the killset and update - // the genset accordingly. - // - // Even though, LSLocations are canonicalized, we still need to consult - // alias analysis to determine whether 2 LSLocations are disjointed. - LSLocation &R = LocationVault[bit]; - for (unsigned i = 0; i < S->LocationNum; ++i) { - if (!S->BBMaxStoreSet.test(i)) - continue; - // Do nothing if the read location NoAlias with the current location. - LSLocation &L = LocationVault[i]; - if (!L.isMayAliasLSLocation(R, AA)) - continue; - // Update the genset and kill set. - S->startTrackingLocation(S->BBKillSet, i); - S->stopTrackingLocation(S->BBGenSet, i); - } -} - -void DSEContext::processRead(SILInstruction *I, SILValue Mem, DSEKind Kind) { - auto *S = getBlockState(I); - // Construct a LSLocation to represent the memory read by this instruction. - // NOTE: The base will point to the actual object this inst is accessing, - // not this particular field. - // - // e.g. %1 = alloc_stack $S - // %2 = struct_element_addr %1, #a - // %3 = load %2 : $*Int - // - // Base will point to %1, but not %2. Projection path will indicate which - // field is accessed. - // - // This will make comparison between locations easier. This eases the - // implementation of intersection operator in the data flow. - LSLocation L; - if (BaseToLocIndex.find(Mem) != BaseToLocIndex.end()) { - L = BaseToLocIndex[Mem]; - } else { - SILValue UO = getUnderlyingObject(Mem); - L = LSLocation(UO, ProjectionPath::getProjectionPath(UO, Mem)); - } - - // If we can't figure out the Base or Projection Path for the read instruction, - // process it as an unknown memory instruction for now. - if (!L.isValid()) { - processUnknownReadInst(I, Kind); - return; - } - - // Expand the given Mem into individual fields and process them as separate - // reads. - LSLocationList Locs; - LSLocation::expand(L, &I->getModule(), - TypeExpansionContext(*I->getFunction()), Locs, TE); - - // Are we building the genset and killset. - if (isBuildingGenKillSet(Kind)) { - for (auto &E : Locs) { - // Only building the gen and kill sets for now. - processReadForGenKillSet(S, getLocationBit(E)); - } - return; - } - - // Are we performing the actual DSE. - if (isPerformingDSE(Kind)) { - for (auto &E : Locs) { - // This is the last iteration, compute BBWriteSetOut and perform DSE. - processReadForDSE(S, getLocationBit(E)); - } - return; - } - - llvm_unreachable("Unknown DSE compute kind"); -} - -bool DSEContext::processWriteForDSE(BlockState *S, unsigned bit) { - // If a tracked store must aliases with this store, then this store is dead. - bool StoreDead = false; - LSLocation &R = LocationVault[bit]; - for (unsigned i = 0; i < S->LocationNum; ++i) { - if (!S->isTrackingLocation(S->BBWriteSetMid, i)) - continue; - // If 2 locations may alias, we can still keep both stores. - LSLocation &L = LocationVault[i]; - if (!L.isMustAliasLSLocation(R, AA)) - continue; - // There is a must alias store. No need to check further. - StoreDead = true; - break; - } - - // Track this new store. - S->startTrackingLocation(S->BBWriteSetMid, bit); - return StoreDead; -} - -void DSEContext::processWriteForGenKillSet(BlockState *S, unsigned bit) { - S->startTrackingLocation(S->BBGenSet, bit); -} - -void DSEContext::processWriteForMaxStoreSet(BlockState *S, unsigned bit) { - S->startTrackingLocation(S->BBMaxStoreSet, bit); -} - -void DSEContext::processWrite(SILInstruction *I, SILValue Val, SILValue Mem, - DSEKind Kind) { - auto *S = getBlockState(I); - // Construct a LSLocation to represent the memory read by this instruction. - // NOTE: The base will point to the actual object this inst is accessing, - // not this particular field. - // - // e.g. %1 = alloc_stack $S - // %2 = struct_element_addr %1, #a - // store %3 to %2 : $*Int - // - // Base will point to %1, but not %2. Projection path will indicate which - // field is accessed. - // - // This will make comparison between locations easier. This eases the - // implementation of intersection operator in the data flow. - LSLocation L; - if (BaseToLocIndex.find(Mem) != BaseToLocIndex.end()) { - L = BaseToLocIndex[Mem]; - } else { - SILValue UO = getUnderlyingObject(Mem); - L = LSLocation(UO, ProjectionPath::getProjectionPath(UO, Mem)); - } - - // If we can't figure out the Base or Projection Path for the store - // instruction, simply ignore it. - if (!L.isValid()) - return; - - // Expand the given Mem into individual fields and process them as separate - // writes. - bool Dead = true; - LSLocationList Locs; - LSLocation::expand(L, Mod, TypeExpansionContext(*I->getFunction()), Locs, TE); - SmallBitVector V(Locs.size()); - - // Are we computing max store set. - if (isComputeMaxStoreSet(Kind)) { - for (auto &E : Locs) { - // Update the max store set for the basic block. - processWriteForMaxStoreSet(S, getLocationBit(E)); - } - return; - } - - // Are we computing genset and killset. - if (isBuildingGenKillSet(Kind)) { - for (auto &E : Locs) { - // Only building the gen and kill sets here. - processWriteForGenKillSet(S, getLocationBit(E)); - } - // Data flow has not stabilized, do not perform the DSE just yet. - return; - } - - // We are doing the actual DSE. - assert(isPerformingDSE(Kind) && "Invalid computation kind"); - unsigned idx = 0; - for (auto &E : Locs) { - // This is the last iteration, compute BBWriteSetOut and perform the dead - // store elimination. - if (processWriteForDSE(S, getLocationBit(E))) - V.set(idx); - Dead &= V.test(idx); - ++idx; - } - - // Fully dead store - stores to all the components are dead, therefore this - // instruction is dead. - if (Dead) { - LLVM_DEBUG(llvm::dbgs() << "Instruction Dead: " << *I << "\n"); - S->DeadStores.push_back(I); - ++NumDeadStores; - return; - } - - // Partial dead store - stores to some locations are dead, but not all. This - // is a partially dead store. Also at this point we know what locations are - // dead. - LSLocationList Alives; - if (V.any()) { - // Take out locations that are dead. - for (unsigned i = 0; i < V.size(); ++i) { - if (V.test(i)) - continue; - // This location is alive. - Alives.push_back(Locs[i]); - } - - // Try to create as few aggregated stores as possible out of the locations. - LSLocation::reduce(L, Mod, TypeExpansionContext(*I->getFunction()), Alives); - - // Oops, we have too many smaller stores generated, bail out. - if (Alives.size() > MaxPartialStoreCount) - return; - - // At this point, we are performing a partial dead store elimination. - // - // Locations here have a projection path from their Base, but this - // particular instruction may not be accessing the base, so we need to - // *rebase* the locations w.r.t. to the current instruction. - SILValue B = Locs[0].getBase(); - llvm::Optional BP = - ProjectionPath::getProjectionPath(B, Mem); - // Strip off the projection path from base to the accessed field. - for (auto &X : Alives) { - X.removePathPrefix(BP); - } - - // We merely setup the remaining live stores, but do not materialize in IR - // yet, These stores will be materialized before the algorithm exits. - for (auto &X : Alives) { - SILValue Value = X.getPath()->createExtract(Val, I, true); - SILValue Addr = X.getPath()->createExtract(Mem, I, false); - S->LiveAddr.insert(Addr); - S->LiveStores[Addr] = Value; - } - - // Lastly, mark the old store as dead. - LLVM_DEBUG(llvm::dbgs() << "Instruction Partially Dead: " << *I << "\n"); - S->DeadStores.push_back(I); - ++NumPartialDeadStores; - } -} - -void DSEContext::processLoadInst(SILInstruction *I, DSEKind Kind) { - processRead(I, cast(I)->getOperand(), Kind); -} - -void DSEContext::processStoreInst(SILInstruction *I, DSEKind Kind) { - auto *SI = cast(I); - processWrite(I, SI->getSrc(), SI->getDest(), Kind); -} - -void DSEContext::processDebugValueAddrInstForGenKillSet(SILInstruction *I) { - BlockState *S = getBlockState(I); - SILValue Mem = cast(I)->getOperand(); - for (unsigned i = 0; i < S->LocationNum; ++i) { - if (!S->BBMaxStoreSet.test(i)) - continue; - if (AA->isNoAlias(Mem, LocationVault[i].getBase())) - continue; - S->stopTrackingLocation(S->BBGenSet, i); - S->startTrackingLocation(S->BBKillSet, i); - } -} - -void DSEContext::processDebugValueAddrInstForDSE(SILInstruction *I) { - BlockState *S = getBlockState(I); - SILValue Mem = cast(I)->getOperand(); - for (unsigned i = 0; i < S->LocationNum; ++i) { - if (!S->isTrackingLocation(S->BBWriteSetMid, i)) - continue; - if (AA->isNoAlias(Mem, LocationVault[i].getBase())) - continue; - S->stopTrackingLocation(S->BBWriteSetMid, i); - } -} - -void DSEContext::processDebugValueAddrInst(SILInstruction *I, DSEKind Kind) { - // Are we building genset and killset. - if (isBuildingGenKillSet(Kind)) { - processDebugValueAddrInstForGenKillSet(I); - return; - } - - // Are we performing dead store elimination. - if (isPerformingDSE(Kind)) { - processDebugValueAddrInstForDSE(I); - return; - } - - llvm_unreachable("Unknown DSE compute kind"); -} - -void DSEContext::processUnknownReadInstForGenKillSet(SILInstruction *I) { - BlockState *S = getBlockState(I); - for (unsigned i = 0; i < S->LocationNum; ++i) { - if (!S->BBMaxStoreSet.test(i)) - continue; - if (!AA->mayReadFromMemory(I, LocationVault[i].getBase())) - continue; - // Update the genset and kill set. - S->startTrackingLocation(S->BBKillSet, i); - S->stopTrackingLocation(S->BBGenSet, i); - } -} - -void DSEContext::processUnknownReadInstForDSE(SILInstruction *I) { - BlockState *S = getBlockState(I); - for (unsigned i = 0; i < S->LocationNum; ++i) { - if (!S->isTrackingLocation(S->BBWriteSetMid, i)) - continue; - if (!AA->mayReadFromMemory(I, LocationVault[i].getBase())) - continue; - S->stopTrackingLocation(S->BBWriteSetMid, i); - } -} - -void DSEContext::processUnknownReadInst(SILInstruction *I, DSEKind Kind) { - // If this is a release on a guaranteed parameter, it can not call deinit, - // which might read or write memory. - if (isIntermediateRelease(I, EAFI)) - return; - - // Are we building genset and killset. - if (isBuildingGenKillSet(Kind)) { - processUnknownReadInstForGenKillSet(I); - return; - } - - // Are we performing dead store elimination. - if (isPerformingDSE(Kind)) { - processUnknownReadInstForDSE(I); - return; - } - - llvm_unreachable("Unknown DSE compute kind"); -} - -void DSEContext::processInstruction(SILInstruction *I, DSEKind Kind) { - // If this instruction has side effects, but is inert from a store - // perspective, skip it. - if (isDeadStoreInertInstruction(I)) - return; - - // A set of ad-hoc rules to process instructions. - if (isa(I)) { - processLoadInst(I, Kind); - } else if (isa(I)) { - processStoreInst(I, Kind); - } else if (DebugValueInst::hasAddrVal(I)) { - processDebugValueAddrInst(I, Kind); - } else if (I->mayReadFromMemory()) { - processUnknownReadInst(I, Kind); - } - - // Check whether this instruction will invalidate any other locations. - for (auto result : I->getResults()) - invalidateBase(result, getBlockState(I), Kind); -} - -void DSEContext::runIterativeDSE() { - // Generate the genset and killset for each basic block. We can process the - // basic blocks in any order. - // - // We also compute the max store set at the beginning of the basic block. - // - auto *PO = PM->getAnalysis()->get(F); - for (SILBasicBlock *B : PO->getPostOrder()) { - processBasicBlockForGenKillSet(B); - } - - // Process each basic block with the gen and kill set. Every time the - // BBWriteSetIn of a basic block changes, the optimization is rerun on its - // predecessors. - BasicBlockWorklist WorkList(F); - // Push into reverse post order so that we can pop from the back and get - // post order. - for (SILBasicBlock *B : PO->getReversePostOrder()) { - WorkList.push(B); - } - while (SILBasicBlock *BB = WorkList.popAndForget()) { - if (processBasicBlockWithGenKillSet(BB)) { - for (SILBasicBlock *pred : BB->getPredecessorBlocks()) { - WorkList.pushIfNotVisited(pred); - } - } - } -} - -bool DSEContext::run() { - int numLoads = 0, numStores = 0; - bool immutableLoadsFound = false; - // Walk over the function and find all the locations accessed by - // this function. - LSLocation::enumerateLSLocations(*F, LocationVault, LocToBitIndex, - BaseToLocIndex, TE, - /*stopAtImmutable*/ false, - numLoads, numStores, immutableLoadsFound); - - // Check how to optimize this function. - ProcessKind Kind = getProcessFunctionKind(numStores); - - // We do not optimize this function at all. - if (Kind == ProcessKind::ProcessNone) - return false; - - // Do we run a pessimistic data flow ? - const bool Optimistic = (Kind == ProcessKind::ProcessOptimistic); - - // For all basic blocks in the function, initialize a BB state. - // - // Initialize the BBToLocState mapping. - unsigned LocationNum = this->getLocationVault().size(); - - if (LocationNum > 500) { - // Emergency exit to avoid bad compile time problems in rare corner cases. - // This limit is more than enough for "real world" code. - // Even large functions have < 100 locations. - // But in some corner cases - especially in generated code - we can run - // into quadratic complexity for large functions. - // TODO: implement DSE with a better (non-quadratic) algorithm - return false; - } - - for (auto bs : BBToLocState) { - bs.data.init(&bs.block, LocationNum, Optimistic); - bs.data.initStoreSetAtEndOfBlock(*this); - } - - // We perform dead store elimination in the following phases. - // - // Phase 1. we compute the max store set at the beginning of the basic block. - // - // Phase 2. we compute the genset and killset for every basic block. - // - // Phase 3. we run the data flow with the genset and killset until - // BBWriteSetIns stop changing. - // - // Phase 4. we run the data flow for the last iteration and perform the DSE. - // - // Phase 5. we remove the dead stores. - // - // Phase 1 - 3 are only performed when we know the data flow will not - // converge in a single iteration. Otherwise, we only run phase 4 and 5 - // on the function. - - // We need to run the iterative data flow on the function. - if (Optimistic) { - runIterativeDSE(); - } - - // The data flow has stabilized, run one last iteration over all the basic - // blocks and try to remove dead stores. - // Is this a one iteration function. - auto *PO = PM->getAnalysis()->get(F); - for (SILBasicBlock *B : PO->getPostOrder()) { - processBasicBlockForDSE(B, Optimistic); - } - - // Finally, delete the dead stores and create the live stores. - bool Changed = false; - for (auto bs : BBToLocState) { - // Create the stores that are alive due to partial dead stores. - for (auto &X : bs.data.LiveAddr) { - Changed = true; - auto I = bs.data.LiveStores.find(X); - SILInstruction *Inst = I->first->getDefiningInstruction(); - auto *IT = &*std::next(Inst->getIterator()); - SILBuilderWithScope Builder(IT); - Builder.createStore(Inst->getLoc(), I->second, I->first, - StoreOwnershipQualifier::Unqualified); - } - // Delete the dead stores. - for (auto &I : getBlockState(&bs.block)->DeadStores) { - Changed = true; - LLVM_DEBUG(llvm::dbgs() << "*** Removing: " << *I << " ***\n"); - // This way, we get rid of pass dependence on DCE. - recursivelyDeleteTriviallyDeadInstructions(I, true); - } - } - - return Changed; -} - -//===----------------------------------------------------------------------===// -// Top Level Entry Point -//===----------------------------------------------------------------------===// - -namespace { - -class DeadStoreElimination : public SILFunctionTransform { -public: - /// The entry point to the transformation. - void run() override { - SILFunction *F = getFunction(); - LLVM_DEBUG(llvm::dbgs() << "*** DSE on function: " << F->getName() - << " ***\n"); - - auto *AA = PM->getAnalysis(F); - auto *TE = PM->getAnalysis(); - auto *EAFI = PM->getAnalysis()->get(F); - - // The allocator we are using. - llvm::SpecificBumpPtrAllocator BPA; - - DSEContext DSE(F, &F->getModule(), PM, AA, TE, EAFI, BPA); - if (DSE.run()) { - invalidateAnalysis(SILAnalysis::InvalidationKind::Instructions); - } - } -}; - -} // end anonymous namespace - -SILTransform *swift::createDeadStoreElimination() { - return new DeadStoreElimination(); -} diff --git a/test/SILOptimizer/dead_store_elim.sil b/test/SILOptimizer/dead_store_elim.sil index a3a415d6bd4a0..33fff65ac6869 100644 --- a/test/SILOptimizer/dead_store_elim.sil +++ b/test/SILOptimizer/dead_store_elim.sil @@ -1,4 +1,4 @@ -// RUN: %target-sil-opt %s -dead-store-elim -max-partial-store-count=2 -enable-sil-verify-all | %FileCheck %s +// RUN: %target-sil-opt %s -dead-store-elimination -enable-sil-verify-all | %FileCheck %s // REQUIRES: swift_in_compiler @@ -106,6 +106,11 @@ class foo { init() } +struct IntAndFoo { + var i: Int + var f: foo +} + class AB { var value: Int init(value: Int) @@ -254,6 +259,8 @@ bb0(%0 : $B): // CHECK-NEXT: integer_literal // CHECK-NEXT: builtin // CHECK-NEXT: tuple_extract +// CHECK-NEXT: builtin +// CHECK-NEXT: tuple_extract // CHECK-NEXT: {{ store}} // CHECK-NEXT: tuple // CHECK-NEXT: return @@ -1542,3 +1549,67 @@ bb0: dealloc_stack_ref %1 : $foo return %3 : $Int } + +sil @read_from_argument : $@convention(thin) (@in_guaranteed Int) -> () + +// CHECK-LABEL: sil @test_ref_tail_addr : +// CHECK-NOT: store +// CHECK: end sil function 'test_ref_tail_addr' +sil @test_ref_tail_addr : $@convention(thin) (Int) -> () { +bb0(%0 : $Int): + %1 = integer_literal $Builtin.Word, 1 + %2 = alloc_ref [stack] [tail_elems $Int * %1 : $Builtin.Word] $foo + %3 = ref_element_addr %2 : $foo, #foo.a + store %0 to %3 : $*Int + %5 = ref_tail_addr %2 : $foo, $Int + %6 = function_ref @read_from_argument : $@convention(thin) (@in_guaranteed Int) -> () + %7 = apply %6(%5) : $@convention(thin) (@in_guaranteed Int) -> () + dealloc_stack_ref %2 : $foo + %r = tuple () + return %r : $() +} + +// CHECK-LABEL: sil @test_split_tuple : +// CHECK: [[T1:%.*]] = tuple_element_addr %1 : $*(Int, Int), 0 +// CHECK-NEXT: store {{.*}} to [[T1]] +// CHECK-NOT: store +// CHECK: fix_lifetime +// CHECK-NEXT: [[T2:%.*]] = tuple_element_addr %1 : $*(Int, Int), 1 +// CHECK-NEXT: store %0 to [[T2]] +// CHECK-NOT: store +// CHECK: end sil function 'test_split_tuple' +sil @test_split_tuple : $@convention(thin) (Int, @inout (Int, Int)) -> () { +bb0(%0 : $Int, %1 : $*(Int, Int)): + %2 = tuple (%0 : $Int, %0 : $Int) + store %2 to %1 : $*(Int, Int) + fix_lifetime %0 : $Int + %4 = tuple_element_addr %1 : $*(Int, Int), 1 + store %0 to %4 : $*Int + %r = tuple () + return %r : $() +} + +// CHECK-LABEL: sil [ossa] @test_ossa_not_trivial : +// CHECK: store %0 +// CHECK: store %1 +// CHECK: end sil function 'test_ossa_not_trivial' +sil [ossa] @test_ossa_not_trivial : $@convention(thin) (@owned foo, @owned foo, @inout foo) -> () { +bb0(%0 : @owned $foo, %1 : @owned $foo, %2 : $*foo): + store %0 to [assign] %2 : $*foo + store %1 to [assign] %2 : $*foo + %r = tuple () + return %r : $() +} + +// CHECK-LABEL: sil [ossa] @test_ossa_partial_trivial : +// CHECK-NOT: store %0 +// CHECK: store %1 +// CHECK: end sil function 'test_ossa_partial_trivial' +sil [ossa] @test_ossa_partial_trivial : $@convention(thin) (Int, @owned IntAndFoo, @inout IntAndFoo) -> () { +bb0(%0 : $Int, %1 : @owned $IntAndFoo, %2 : $*IntAndFoo): + %3 = struct_element_addr %2 : $*IntAndFoo, #IntAndFoo.i + store %0 to [trivial] %3 : $*Int + store %1 to [assign] %2 : $*IntAndFoo + %r = tuple () + return %r : $() +} diff --git a/test/SILOptimizer/devirtualize_protocol_composition_two_stores.sil b/test/SILOptimizer/devirtualize_protocol_composition_two_stores.sil index 34c177230e0be..70b9b1da2c419 100644 --- a/test/SILOptimizer/devirtualize_protocol_composition_two_stores.sil +++ b/test/SILOptimizer/devirtualize_protocol_composition_two_stores.sil @@ -1,5 +1,7 @@ // RUN: %target-sil-opt -O -wmo -enable-sil-verify-all %s | %FileCheck %s +// REQUIRES: swift_in_compiler + sil_stage canonical import Builtin diff --git a/test/SILOptimizer/escape_analysis_dead_store.sil b/test/SILOptimizer/escape_analysis_dead_store.sil index 4cbed3a663660..f337be4ce7435 100644 --- a/test/SILOptimizer/escape_analysis_dead_store.sil +++ b/test/SILOptimizer/escape_analysis_dead_store.sil @@ -1,4 +1,4 @@ -// RUN: %target-sil-opt %s -dead-store-elim -enable-sil-verify-all | %FileCheck %s +// RUN: %target-sil-opt %s -dead-store-elimination -enable-sil-verify-all | %FileCheck %s // REQUIRES: swift_in_compiler diff --git a/test/SILOptimizer/redundant_load_and_dead_store_elim.sil b/test/SILOptimizer/redundant_load_and_dead_store_elim.sil index 0cfa82802317c..34f83c5bb5729 100644 --- a/test/SILOptimizer/redundant_load_and_dead_store_elim.sil +++ b/test/SILOptimizer/redundant_load_and_dead_store_elim.sil @@ -1,4 +1,6 @@ -// RUN: %target-sil-opt -enable-sil-verify-all %s -redundant-load-elim -dead-store-elim | %FileCheck %s +// RUN: %target-sil-opt -enable-sil-verify-all %s -redundant-load-elim -dead-store-elimination | %FileCheck %s + +// REQUIRES: swift_in_compiler // NOTE, the order redundant-load and dead-store are ran is important. we have a pass dependence for some // of the tests to work.