Skip to content

Commit dd78dc7

Browse files
committed
Optimizer: add an optimization to remove copy_value of a borrowed value.
It removes a `copy_value` where the source is a guaranteed value, if possible: ``` %1 = copy_value %0 // %0 = a guaranteed value // uses of %1 destroy_value %1 // borrow scope of %0 is still valid here ``` -> ``` // uses of %0 ``` This optimization is very similar to the LoadCopyToBorrow optimization. Therefore I merged both optimizations into a single file and renamed it to "CopyToBorrowOptimization".
1 parent 1fcfa11 commit dd78dc7

File tree

7 files changed

+463
-298
lines changed

7 files changed

+463
-298
lines changed

SwiftCompilerSources/Sources/Optimizer/FunctionPasses/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ swift_compiler_sources(Optimizer
1515
ClosureSpecialization.swift
1616
ComputeEscapeEffects.swift
1717
ComputeSideEffects.swift
18+
CopyToBorrowOptimization.swift
1819
DeadStoreElimination.swift
1920
DeinitDevirtualizer.swift
2021
InitializeStaticGlobals.swift
2122
LetPropertyLowering.swift
2223
LifetimeDependenceDiagnostics.swift
2324
LifetimeDependenceInsertion.swift
2425
LifetimeDependenceScopeFixup.swift
25-
LoadCopyToBorrowOptimization.swift
2626
ObjectOutliner.swift
2727
ObjCBridgingOptimization.swift
2828
MergeCondFails.swift
Lines changed: 383 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
1+
//===--- CopyToBorrowOptimization.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+
/// 1. 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+
/// 2. removes a `copy_value` where the source is a guaranteed value, if possible:
30+
///
31+
/// ```
32+
/// %1 = copy_value %0 // %0 = a guaranteed value
33+
/// // uses of %1
34+
/// destroy_value %1 // borrow scope of %0 is still valid here
35+
/// ```
36+
/// ->
37+
/// ```
38+
/// // uses of %0
39+
/// ```
40+
41+
/// The optimization can be done if:
42+
/// * In caseof a `load`: during the (forward-extended) lifetime of the loaded value the
43+
/// memory location is not changed.
44+
/// * In case of a `copy_value`: the (guaranteed) lifetime of the source operand extends
45+
/// the lifetime of the copied value.
46+
/// * All (forward-extended) uses of the load or copy support guaranteed ownership.
47+
/// * The (forward-extended) lifetime of the load or copy ends with `destroy_value`(s).
48+
///
49+
let copyToBorrowOptimization = FunctionPass(name: "copy-to-borrow-optimization") {
50+
(function: Function, context: FunctionPassContext) in
51+
52+
if !function.hasOwnership {
53+
return
54+
}
55+
56+
for inst in function.instructions {
57+
switch inst {
58+
case let load as LoadInst:
59+
optimize(load: load, context)
60+
case let copy as CopyValueInst:
61+
optimize(copy: copy, context)
62+
default:
63+
break
64+
}
65+
}
66+
}
67+
68+
private func optimize(load: LoadInst, _ context: FunctionPassContext) {
69+
if load.loadOwnership != .copy {
70+
return
71+
}
72+
73+
var collectedUses = Uses(context)
74+
defer { collectedUses.deinitialize() }
75+
if !collectedUses.collectUses(of: load) {
76+
return
77+
}
78+
79+
if mayWrite(toAddressOf: load,
80+
within: collectedUses.destroys,
81+
usersInDeadEndBlocks: collectedUses.usersInDeadEndBlocks,
82+
context)
83+
{
84+
return
85+
}
86+
87+
load.replaceWithLoadBorrow(collectedUses: collectedUses)
88+
}
89+
90+
private func optimize(copy: CopyValueInst, _ context: FunctionPassContext) {
91+
if copy.fromValue.ownership != .guaranteed {
92+
return
93+
}
94+
95+
var collectedUses = Uses(context)
96+
defer { collectedUses.deinitialize() }
97+
if !collectedUses.collectUses(of: copy) {
98+
return
99+
}
100+
101+
var liverange = InstructionRange(begin: copy, ends: collectedUses.destroys, context)
102+
defer { liverange.deinitialize() }
103+
104+
if !liverange.isFullyContainedIn(borrowScopeOf: copy.fromValue.lookThroughForwardingInstructions) {
105+
return
106+
}
107+
108+
remove(copy: copy, collectedUses: collectedUses, liverange: liverange)
109+
}
110+
111+
private struct Uses {
112+
let context: FunctionPassContext
113+
114+
// Operand of all forwarding instructions, which - if possible - are converted from "owned" to "guaranteed"
115+
private(set) var forwardingUses: Stack<Operand>
116+
117+
// All destroys of the load/copy_value and its forwarded values.
118+
private(set) var destroys: Stack<DestroyValueInst>
119+
120+
// Exit blocks of the load/copy_value's liverange which don't have a destroy.
121+
// Those are successor blocks of terminators, like `switch_enum`, which do _not_ forward the value.
122+
// E.g. the none-case of a switch_enum of an Optional.
123+
private(set) var nonDestroyingLiverangeExits: Stack<BasicBlock>
124+
125+
private(set) var usersInDeadEndBlocks: Stack<Instruction>
126+
127+
init(_ context: FunctionPassContext) {
128+
self.context = context
129+
self.forwardingUses = Stack(context)
130+
self.destroys = Stack(context)
131+
self.nonDestroyingLiverangeExits = Stack(context)
132+
self.usersInDeadEndBlocks = Stack(context)
133+
}
134+
135+
mutating func collectUses(of initialValue: SingleValueInstruction) -> Bool {
136+
var worklist = ValueWorklist(context)
137+
defer { worklist.deinitialize() }
138+
139+
// If the load/copy_value is immediately followed by a single `move_value`, use the moved value.
140+
// Note that `move_value` is _not_ a forwarding instruction.
141+
worklist.pushIfNotVisited(initialValue.singleMoveValueUser ?? initialValue)
142+
143+
while let value = worklist.pop() {
144+
for use in value.uses.endingLifetime {
145+
switch use.instruction {
146+
case let destroy as DestroyValueInst:
147+
destroys.append(destroy)
148+
149+
case let forwardingInst as ForwardingInstruction where forwardingInst.canChangeToGuaranteedOwnership:
150+
forwardingUses.append(use)
151+
findNonDestroyingLiverangeExits(of: forwardingInst)
152+
worklist.pushIfNotVisited(contentsOf: forwardingInst.forwardedResults.lazy.filter { $0.ownership == .owned})
153+
default:
154+
return false
155+
}
156+
}
157+
// Get potential additional uses in dead-end blocks for which a final destroy is missing.
158+
// In such a case the dataflow would _not_ visit potential writes to the load's memory location.
159+
// In the following example, the `load [copy]` must not be converted to a `load_borrow`:
160+
//
161+
// %1 = load [copy] %0
162+
// ...
163+
// store %2 to %0
164+
// ...
165+
// use of %1 // additional use: the lifetime of %1 ends here
166+
// ... // no destroy of %1!
167+
// unreachable
168+
//
169+
// TODO: we can remove this once with have completed OSSA lifetimes throughout the SIL pipeline.
170+
findAdditionalUsesInDeadEndBlocks(of: value)
171+
}
172+
return true
173+
}
174+
175+
private mutating func findNonDestroyingLiverangeExits(of forwardingInst: ForwardingInstruction) {
176+
if let termInst = forwardingInst as? TermInst {
177+
// A terminator instruction can implicitly end the lifetime of its operand in a success block,
178+
// e.g. a `switch_enum` with a non-payload case block. Such success blocks need an `end_borrow`, though.
179+
for succ in termInst.successors where !succ.arguments.contains(where: {$0.ownership == .owned}) {
180+
nonDestroyingLiverangeExits.append(succ)
181+
}
182+
}
183+
}
184+
185+
private mutating func findAdditionalUsesInDeadEndBlocks(of value: Value) {
186+
var users = Stack<Instruction>(context)
187+
defer { users.deinitialize() }
188+
189+
// Finds all uses except destroy_value.
190+
var visitor = InteriorUseWalker(definingValue: value, ignoreEscape: true, visitInnerUses: true, context) {
191+
let user = $0.instruction
192+
if !(user is DestroyValueInst) {
193+
users.append(user)
194+
}
195+
return .continueWalk
196+
}
197+
defer { visitor.deinitialize() }
198+
199+
_ = visitor.visitUses()
200+
usersInDeadEndBlocks.append(contentsOf: users)
201+
}
202+
203+
mutating func deinitialize() {
204+
forwardingUses.deinitialize()
205+
destroys.deinitialize()
206+
nonDestroyingLiverangeExits.deinitialize()
207+
usersInDeadEndBlocks.deinitialize()
208+
}
209+
}
210+
211+
private func mayWrite(
212+
toAddressOf load: LoadInst,
213+
within destroys: Stack<DestroyValueInst>,
214+
usersInDeadEndBlocks: Stack<Instruction>,
215+
_ context: FunctionPassContext
216+
) -> Bool {
217+
let aliasAnalysis = context.aliasAnalysis
218+
var worklist = InstructionWorklist(context)
219+
defer { worklist.deinitialize() }
220+
221+
for destroy in destroys {
222+
worklist.pushPredecessors(of: destroy, ignoring: load)
223+
}
224+
worklist.pushIfNotVisited(contentsOf: usersInDeadEndBlocks)
225+
226+
// Visit all instructions starting from the destroys in backward order.
227+
while let inst = worklist.pop() {
228+
if inst.mayWrite(toAddress: load.address, aliasAnalysis) {
229+
return true
230+
}
231+
worklist.pushPredecessors(of: inst, ignoring: load)
232+
}
233+
return false
234+
}
235+
236+
private extension LoadInst {
237+
func replaceWithLoadBorrow(collectedUses: Uses) {
238+
let context = collectedUses.context
239+
let builder = Builder(before: self, context)
240+
let loadBorrow = builder.createLoadBorrow(fromAddress: address)
241+
242+
var liverange = InstructionRange(begin: self, ends: collectedUses.destroys, context)
243+
defer { liverange.deinitialize() }
244+
245+
replaceMoveWithBorrow(of: self, replacedBy: loadBorrow, liverange: liverange, collectedUses: collectedUses)
246+
createEndBorrows(for: loadBorrow, atEndOf: liverange, collectedUses: collectedUses)
247+
248+
uses.replaceAll(with: loadBorrow, context)
249+
context.erase(instruction: self)
250+
251+
for forwardingUse in collectedUses.forwardingUses {
252+
forwardingUse.changeOwnership(from: .owned, to: .guaranteed, context)
253+
}
254+
context.erase(instructions: collectedUses.destroys)
255+
}
256+
}
257+
258+
private func remove(copy: CopyValueInst, collectedUses: Uses, liverange: InstructionRange) {
259+
let context = collectedUses.context
260+
replaceMoveWithBorrow(of: copy, replacedBy: copy.fromValue, liverange: liverange, collectedUses: collectedUses)
261+
copy.uses.replaceAll(with: copy.fromValue, context)
262+
context.erase(instruction: copy)
263+
264+
for forwardingUse in collectedUses.forwardingUses {
265+
forwardingUse.changeOwnership(from: .owned, to: .guaranteed, context)
266+
}
267+
context.erase(instructions: collectedUses.destroys)
268+
}
269+
270+
// Handle the special case if the `load` or `copy_valuw` is immediately followed by a single `move_value`.
271+
// In this case we have to preserve the move's flags by inserting a `begin_borrow` with the same flags.
272+
// For example:
273+
//
274+
// %1 = load [copy] %0
275+
// %2 = move_value [lexical] %1
276+
// ...
277+
// destroy_value %2
278+
// ->
279+
// %1 = load_borrow %0
280+
// %2 = begin_borrow [lexical] %1
281+
// ...
282+
// end_borrow %2
283+
// end_borrow %1
284+
//
285+
private func replaceMoveWithBorrow(
286+
of value: Value,
287+
replacedBy newValue: Value,
288+
liverange: InstructionRange,
289+
collectedUses: Uses
290+
) {
291+
guard let moveInst = value.singleMoveValueUser else {
292+
return
293+
}
294+
let context = collectedUses.context
295+
296+
// An inner borrow is needed to keep the flags of the `move_value`.
297+
let builder = Builder(before: moveInst, context)
298+
let bbi = builder.createBeginBorrow(of: newValue,
299+
isLexical: moveInst.isLexical,
300+
hasPointerEscape: moveInst.hasPointerEscape,
301+
isFromVarDecl: moveInst.isFromVarDecl)
302+
moveInst.uses.replaceAll(with: bbi, context)
303+
context.erase(instruction: moveInst)
304+
createEndBorrows(for: bbi, atEndOf: liverange, collectedUses: collectedUses)
305+
}
306+
307+
private func createEndBorrows(for beginBorrow: Value, atEndOf liverange: InstructionRange, collectedUses: Uses) {
308+
let context = collectedUses.context
309+
310+
// There can be multiple destroys in a row in case of decomposing an aggregate, e.g.
311+
// %1 = load [copy] %0
312+
// ...
313+
// (%2, %3) = destructure_struct %1
314+
// destroy_value %2
315+
// destroy_value %3 // The final destroy. Here we need to create the `end_borrow`(s)
316+
//
317+
for destroy in collectedUses.destroys where !liverange.contains(destroy) {
318+
let builder = Builder(before: destroy, context)
319+
builder.createEndBorrow(of: beginBorrow)
320+
}
321+
for liverangeExitBlock in collectedUses.nonDestroyingLiverangeExits where
322+
!liverange.blockRange.contains(liverangeExitBlock)
323+
{
324+
let builder = Builder(atBeginOf: liverangeExitBlock, context)
325+
builder.createEndBorrow(of: beginBorrow)
326+
}
327+
}
328+
329+
private extension InstructionRange {
330+
func isFullyContainedIn(borrowScopeOf value: Value) -> Bool {
331+
guard let beginBorrow = BeginBorrowValue(value.lookThroughForwardingInstructions) else {
332+
return false
333+
}
334+
if case .functionArgument = beginBorrow {
335+
// The lifetime of a guaranteed function argument spans over the whole function.
336+
return true
337+
}
338+
for endOp in beginBorrow.scopeEndingOperands {
339+
if self.contains(endOp.instruction) {
340+
return false
341+
}
342+
}
343+
return true
344+
}
345+
}
346+
347+
private extension Value {
348+
var singleMoveValueUser: MoveValueInst? {
349+
uses.ignoreDebugUses.singleUse?.instruction as? MoveValueInst
350+
}
351+
352+
var lookThroughForwardingInstructions: Value {
353+
if let fi = definingInstruction as? ForwardingInstruction,
354+
let forwardedOp = fi.singleForwardedOperand
355+
{
356+
return forwardedOp.value.lookThroughForwardingInstructions
357+
} else if let termResult = TerminatorResult(self),
358+
let fi = termResult.terminator as? ForwardingInstruction,
359+
let forwardedOp = fi.singleForwardedOperand
360+
{
361+
return forwardedOp.value.lookThroughForwardingInstructions
362+
}
363+
return self
364+
}
365+
}
366+
367+
private extension ForwardingInstruction {
368+
var canChangeToGuaranteedOwnership: Bool {
369+
if !preservesReferenceCounts {
370+
return false
371+
}
372+
if !canForwardGuaranteedValues {
373+
return false
374+
}
375+
// For simplicity only support a single owned operand. Otherwise we would have to check if the other
376+
// owned operands stem from `load_borrow`s, too, which we can convert, etc.
377+
let numOwnedOperands = operands.lazy.filter({ $0.value.ownership == .owned }).count
378+
if numOwnedOperands > 1 {
379+
return false
380+
}
381+
return true
382+
}
383+
}

0 commit comments

Comments
 (0)