Skip to content

Commit c06a9e5

Browse files
authored
Merge pull request #76926 from eeckstein/load-copy-opt
Optimizer: improve the load-copy-to-borrow optimization and implement it in swift
2 parents 650189b + eed8645 commit c06a9e5

29 files changed

+968
-1122
lines changed

SwiftCompilerSources/Sources/Optimizer/DataStructures/Set.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ struct ValueSet : IntrusiveSet {
135135
/// This type should be a move-only type, but unfortunately we don't have move-only
136136
/// types yet. Therefore it's needed to call `deinitialize()` explicitly to
137137
/// destruct this data structure, e.g. in a `defer {}` block.
138-
struct InstructionSet : IntrusiveSet {
138+
struct SpecificInstructionSet<InstType: Instruction> : IntrusiveSet {
139139

140140
private let context: BridgedPassContext
141141
private let bridged: BridgedNodeSet
@@ -145,25 +145,25 @@ struct InstructionSet : IntrusiveSet {
145145
self.bridged = self.context.allocNodeSet()
146146
}
147147

148-
func contains(_ inst: Instruction) -> Bool {
148+
func contains(_ inst: InstType) -> Bool {
149149
bridged.containsInstruction(inst.bridged)
150150
}
151151

152152
/// Returns true if `inst` was not contained in the set before inserting.
153153
@discardableResult
154-
mutating func insert(_ inst: Instruction) -> Bool {
154+
mutating func insert(_ inst: InstType) -> Bool {
155155
bridged.insertInstruction(inst.bridged)
156156
}
157157

158-
mutating func erase(_ inst: Instruction) {
158+
mutating func erase(_ inst: InstType) {
159159
bridged.eraseInstruction(inst.bridged)
160160
}
161161

162162
var description: String {
163163
let function = bridged.getFunction().function
164164
var d = "{\n"
165-
for inst in function.instructions {
166-
if contains(inst) {
165+
for i in function.instructions {
166+
if let inst = i as? InstType, contains(inst) {
167167
d += inst.description + "\n"
168168
}
169169
}
@@ -177,6 +177,8 @@ struct InstructionSet : IntrusiveSet {
177177
}
178178
}
179179

180+
typealias InstructionSet = SpecificInstructionSet<Instruction>
181+
180182
/// A set of operands.
181183
///
182184
/// This is an extremely efficient implementation which does not need memory

SwiftCompilerSources/Sources/Optimizer/DataStructures/Worklist.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ struct Worklist<Set: IntrusiveSet> : CustomStringConvertible, NoReflectionChildr
7474

7575
typealias BasicBlockWorklist = Worklist<BasicBlockSet>
7676
typealias InstructionWorklist = Worklist<InstructionSet>
77+
typealias SpecificInstructionWorklist<InstType: Instruction> = Worklist<SpecificInstructionSet<InstType>>
7778
typealias ValueWorklist = Worklist<ValueSet>
7879
typealias OperandWorklist = Worklist<OperandSet>
7980

SwiftCompilerSources/Sources/Optimizer/FunctionPasses/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ swift_compiler_sources(Optimizer
2222
LifetimeDependenceDiagnostics.swift
2323
LifetimeDependenceInsertion.swift
2424
LifetimeDependenceScopeFixup.swift
25+
LoadCopyToBorrowOptimization.swift
2526
ObjectOutliner.swift
2627
ObjCBridgingOptimization.swift
2728
MergeCondFails.swift
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
//===--- LoadCopyToBorrowOptimization.swift --------------------------------==//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SIL
14+
15+
/// Replaces a `load [copy]` with a `load_borrow` if possible.
16+
///
17+
/// ```
18+
/// %1 = load [copy] %0
19+
/// // no writes to %0
20+
/// destroy_value %1
21+
/// ```
22+
/// ->
23+
/// ```
24+
/// %1 = load_borrow %0
25+
/// // no writes to %0
26+
/// end_borrow %1
27+
/// ```
28+
///
29+
/// The optimization can be done if:
30+
/// * During the (forward-extended) lifetime of the loaded value the memory location is not changed.
31+
/// * All (forward-extended) uses of the loaded value support guaranteed ownership.
32+
/// * The (forward-extended) lifetime of the loaded value ends with `destroy_value`(s).
33+
///
34+
let loadCopyToBorrowOptimization = FunctionPass(name: "load-copy-to-borrow-optimization") {
35+
(function: Function, context: FunctionPassContext) in
36+
37+
if !function.hasOwnership {
38+
return
39+
}
40+
41+
for inst in function.instructions {
42+
if let load = inst as? LoadInst {
43+
optimize(load: load, context)
44+
}
45+
}
46+
}
47+
48+
private func optimize(load: LoadInst, _ context: FunctionPassContext) {
49+
if load.loadOwnership != .copy {
50+
return
51+
}
52+
53+
var liverange = Liverange(context)
54+
defer { liverange.deinitialize() }
55+
56+
if !liverange.collectUses(of: load) {
57+
return
58+
}
59+
60+
if liverange.mayWrite(toAddressOf: load) {
61+
return
62+
}
63+
64+
load.replaceWithLoadBorrow(liverange: liverange)
65+
}
66+
67+
private struct Liverange {
68+
let context: FunctionPassContext
69+
70+
// Operand of all forwarding instructions, which - if possible - are converted from "owned" to "guaranteed"
71+
private(set) var forwardingUses: Stack<Operand>
72+
73+
// All destroys of the load and its forwarded values.
74+
private(set) var destroys: Stack<DestroyValueInst>
75+
76+
// Exit blocks of the load's liverange which don't have a destroy.
77+
// Those are successor blocks of terminators, like `switch_enum`, which do _not_ forward the value.
78+
// E.g. the none-case of a switch_enum of an Optional.
79+
private(set) var nonDestroyingLiverangeExits: Stack<BasicBlock>
80+
81+
// All instructions within the liverange. Initialized in `collectUses` and computed in `mayWrite`.
82+
private(set) var instructions: InstructionWorklist
83+
84+
init(_ context: FunctionPassContext) {
85+
self.context = context
86+
self.forwardingUses = Stack(context)
87+
self.destroys = Stack(context)
88+
self.nonDestroyingLiverangeExits = Stack(context)
89+
self.instructions = InstructionWorklist(context)
90+
}
91+
92+
mutating func collectUses(of load: LoadInst) -> Bool {
93+
var worklist = ValueWorklist(context)
94+
defer { worklist.deinitialize() }
95+
96+
// If the `load` is immediately followed by a single `move_value`, use the moved value.
97+
// Note that `move_value` is _not_ a forwarding instruction.
98+
worklist.pushIfNotVisited(load.singleMoveValueUser ?? load)
99+
100+
while let value = worklist.pop() {
101+
for use in value.uses.endingLifetime {
102+
switch use.instruction {
103+
case let destroy as DestroyValueInst:
104+
destroys.append(destroy)
105+
instructions.pushPredecessors(of: destroy, ignoring: load)
106+
107+
case let forwardingInst as ForwardingInstruction where forwardingInst.canChangeToGuaranteedOwnership:
108+
forwardingUses.append(use)
109+
findNonDestroyingLiverangeExits(of: forwardingInst)
110+
worklist.pushIfNotVisited(contentsOf: forwardingInst.forwardedResults.lazy.filter { $0.ownership == .owned})
111+
default:
112+
return false
113+
}
114+
}
115+
// Get potential additional uses in dead-end blocks for which a final destroy is missing.
116+
// In such a case the dataflow would _not_ visit potential writes to the load's memory location.
117+
// In the following example, the `load [copy]` must not be converted to a `load_borrow`:
118+
//
119+
// %1 = load [copy] %0
120+
// ...
121+
// store %2 to %0
122+
// ...
123+
// use of %1 // additional use: the lifetime of %1 ends here
124+
// ... // no destroy of %1!
125+
// unreachable
126+
//
127+
// TODO: we can remove this once with have completed OSSA lifetimes throughout the SIL pipeline.
128+
findAdditionalUsesInDeadEndBlocks(of: value)
129+
}
130+
return true
131+
}
132+
133+
// Constructs the liverange by performing a simple data flow and checks if any instruction in the liverange
134+
// may write to the load's address.
135+
mutating func mayWrite(toAddressOf load: LoadInst) -> Bool {
136+
let aliasAnalysis = context.aliasAnalysis
137+
138+
while let inst = instructions.pop() {
139+
if inst.mayWrite(toAddress: load.address, aliasAnalysis) {
140+
return true
141+
}
142+
instructions.pushPredecessors(of: inst, ignoring: load)
143+
}
144+
return false
145+
}
146+
147+
private mutating func findNonDestroyingLiverangeExits(of forwardingInst: ForwardingInstruction) {
148+
if let termInst = forwardingInst as? TermInst {
149+
// A terminator instruction can implicitly end the lifetime of its operand in a success block,
150+
// e.g. a `switch_enum` with a non-payload case block. Such success blocks need an `end_borrow`, though.
151+
for succ in termInst.successors where !succ.arguments.contains(where: {$0.ownership == .owned}) {
152+
nonDestroyingLiverangeExits.append(succ)
153+
}
154+
}
155+
}
156+
157+
private mutating func findAdditionalUsesInDeadEndBlocks(of value: Value) {
158+
var users = Stack<Instruction>(context)
159+
defer { users.deinitialize() }
160+
161+
// Finds all uses except destroy_value.
162+
var visitor = InteriorUseWalker(definingValue: value, ignoreEscape: true, visitInnerUses: true, context) {
163+
let user = $0.instruction
164+
if !(user is DestroyValueInst) {
165+
users.append(user)
166+
}
167+
return .continueWalk
168+
}
169+
defer { visitor.deinitialize() }
170+
171+
_ = visitor.visitUses()
172+
instructions.pushIfNotVisited(contentsOf: users)
173+
}
174+
175+
mutating func deinitialize() {
176+
forwardingUses.deinitialize()
177+
destroys.deinitialize()
178+
nonDestroyingLiverangeExits.deinitialize()
179+
instructions.deinitialize()
180+
}
181+
}
182+
183+
private extension LoadInst {
184+
var singleMoveValueUser: MoveValueInst? {
185+
uses.ignoreDebugUses.singleUse?.instruction as? MoveValueInst
186+
}
187+
188+
func replaceWithLoadBorrow(liverange: Liverange) {
189+
let context = liverange.context
190+
let builder = Builder(before: self, context)
191+
let loadBorrow = builder.createLoadBorrow(fromAddress: address)
192+
193+
// Handle the special case if the `load` is immediately followed by a single `move_value`.
194+
// In this case we have to preserve the move's flags by inserting a `begin_borrow` with the same flags.
195+
// For example:
196+
//
197+
// %1 = load [copy] %0
198+
// %2 = move_value [lexical] %1
199+
// ...
200+
// destroy_value %2
201+
// ->
202+
// %1 = load_borrow %0
203+
// %2 = begin_borrow [lexical] %1
204+
// ...
205+
// end_borrow %2
206+
// end_borrow %1
207+
//
208+
let innerBorrow: BeginBorrowInst?
209+
if let moveInst = singleMoveValueUser {
210+
// An inner borrow is needed to keep the flags of the `move_value`.
211+
let bbi = builder.createBeginBorrow(of: loadBorrow,
212+
isLexical: moveInst.isLexical,
213+
hasPointerEscape: moveInst.hasPointerEscape,
214+
isFromVarDecl: moveInst.isFromVarDecl)
215+
moveInst.uses.replaceAll(with: bbi, context)
216+
context.erase(instruction: moveInst)
217+
innerBorrow = bbi
218+
} else {
219+
innerBorrow = nil
220+
}
221+
222+
uses.replaceAll(with: loadBorrow, context)
223+
context.erase(instruction: self)
224+
225+
for destroy in liverange.destroys {
226+
context.createEndBorrows(ofInner: innerBorrow, ofOuter: loadBorrow, before: destroy,
227+
ifNotIn: liverange.instructions)
228+
context.erase(instruction: destroy)
229+
}
230+
231+
for forwardingUse in liverange.forwardingUses {
232+
forwardingUse.changeOwnership(from: .owned, to: .guaranteed, context)
233+
}
234+
235+
for liverangeExitBlock in liverange.nonDestroyingLiverangeExits {
236+
context.createEndBorrows(ofInner: innerBorrow, ofOuter: loadBorrow,
237+
before: liverangeExitBlock.instructions.first!,
238+
ifNotIn: liverange.instructions)
239+
}
240+
}
241+
}
242+
243+
private extension FunctionPassContext {
244+
func createEndBorrows(
245+
ofInner innerBorrow: BeginBorrowInst?,
246+
ofOuter outerBorrow: LoadBorrowInst,
247+
before insertionPoint: Instruction,
248+
ifNotIn liverange: InstructionWorklist
249+
) {
250+
// There can be multiple destroys in a row in case of decomposing an aggregate, e.g.
251+
// %1 = load [copy] %0
252+
// ...
253+
// (%2, %3) = destructure_struct %1
254+
// destroy_value %2
255+
// destroy_value %3 // The final destroy. Here we need to create the `end_borrow`(s)
256+
//
257+
if liverange.hasBeenPushed(insertionPoint) {
258+
return
259+
}
260+
let builder = Builder(before: insertionPoint, self)
261+
if let innerBorrow = innerBorrow {
262+
builder.createEndBorrow(of: innerBorrow)
263+
}
264+
builder.createEndBorrow(of: outerBorrow)
265+
}
266+
}
267+
268+
private extension ForwardingInstruction {
269+
var canChangeToGuaranteedOwnership: Bool {
270+
if !preservesReferenceCounts {
271+
return false
272+
}
273+
if !canForwardGuaranteedValues {
274+
return false
275+
}
276+
// For simplicity only support a single owned operand. Otherwise we would have to check if the other
277+
// owned operands stem from `load_borrow`s, too, which we can convert, etc.
278+
let numOwnedOperands = operands.lazy.filter({ $0.value.ownership == .owned }).count
279+
if numOwnedOperands > 1 {
280+
return false
281+
}
282+
return true
283+
}
284+
}

SwiftCompilerSources/Sources/Optimizer/PassManager/Context.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,14 @@ struct FunctionPassContext : MutatingContext {
422422

423423
return buildFn(specializedFunction, nestedFunctionPassContext)
424424
}
425+
426+
/// Makes sure that the lifetime of `value` ends at all control flow paths, even in dead-end blocks.
427+
/// Inserts destroys in dead-end blocks if those are missing.
428+
func completeLifetime(of value: Value) {
429+
if _bridged.completeLifetime(value.bridged) {
430+
notifyInstructionsChanged()
431+
}
432+
}
425433
}
426434

427435
struct SimplifyContext : MutatingContext {
@@ -640,6 +648,12 @@ extension Operand {
640648
func set(to value: Value, _ context: some MutatingContext) {
641649
instruction.setOperand(at: index, to: value, context)
642650
}
651+
652+
func changeOwnership(from: Ownership, to: Ownership, _ context: some MutatingContext) {
653+
context.notifyInstructionsChanged()
654+
bridged.changeOwnership(from._bridged, to._bridged)
655+
context.notifyInstructionChanged(instruction)
656+
}
643657
}
644658

645659
extension Instruction {

SwiftCompilerSources/Sources/Optimizer/PassManager/PassRegistration.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ private func registerSwiftPasses() {
9595
registerPass(lifetimeDependenceDiagnosticsPass, { lifetimeDependenceDiagnosticsPass.run($0) })
9696
registerPass(lifetimeDependenceInsertionPass, { lifetimeDependenceInsertionPass.run($0) })
9797
registerPass(lifetimeDependenceScopeFixupPass, { lifetimeDependenceScopeFixupPass.run($0) })
98+
registerPass(loadCopyToBorrowOptimization, { loadCopyToBorrowOptimization.run($0) })
9899
registerPass(generalClosureSpecialization, { generalClosureSpecialization.run($0) })
99100
registerPass(autodiffClosureSpecialization, { autodiffClosureSpecialization.run($0) })
100101

0 commit comments

Comments
 (0)