Skip to content

Commit c481aba

Browse files
committed
Optimizer: add a new destroy-hoisting optimization
It hoists `destroy_value` instructions without shrinking an object's lifetime. This is done if it can be proved that another copy of a value (either in an SSA value or in memory) keeps the referenced object(s) alive until the original position of the `destroy_value`. ``` %1 = copy_value %0 ... last_use_of %0 // other instructions destroy_value %0 // %1 is still alive here ``` -> ``` %1 = copy_value %0 ... last_use_of %0 destroy_value %0 // other instructions ``` The benefit of this optimization is that it can enable copy-propagation by moving destroys above deinit barries and access scopes.
1 parent 34e1679 commit c481aba

File tree

7 files changed

+773
-0
lines changed

7 files changed

+773
-0
lines changed

SwiftCompilerSources/Sources/Optimizer/FunctionPasses/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ swift_compiler_sources(Optimizer
1717
ComputeSideEffects.swift
1818
DeadStoreElimination.swift
1919
DeinitDevirtualizer.swift
20+
DestroyHoisting.swift
2021
InitializeStaticGlobals.swift
2122
LetPropertyLowering.swift
2223
LifetimeDependenceDiagnostics.swift
Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
//===--- DestroyHoisting.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+
/// Hoists `destroy_value` instructions without shrinking an object's lifetime.
16+
/// This is done if it can be proved that another copy of a value (either in an SSA value or in memory) keeps
17+
/// the referenced object(s) alive until the original position of the `destroy_value`.
18+
///
19+
/// ```
20+
/// %1 = copy_value %0
21+
/// ...
22+
/// last_use_of %0
23+
/// // other instructions
24+
/// destroy_value %0 // %1 is still alive here
25+
/// ```
26+
/// ->
27+
/// ```
28+
/// %1 = copy_value %0
29+
/// ...
30+
/// last_use_of %0
31+
/// destroy_value %0
32+
/// // other instructions
33+
/// ```
34+
///
35+
/// This also works if a copy of the value is kept alive in memory:
36+
///
37+
/// ```
38+
/// %1 = copy_value %0
39+
/// store %1 to [assign] %a
40+
/// ...
41+
/// last_use_of %0
42+
/// // other instructions
43+
/// destroy_value %0 // memory location %a is not modified since the store
44+
/// ```
45+
/// ->
46+
/// ```
47+
/// %1 = copy_value %0
48+
/// store %0 to [assign] %a
49+
/// ...
50+
/// last_use_of %0
51+
/// destroy_value %0
52+
/// // other instructions
53+
/// ```
54+
///
55+
/// The benefit of this optimization is that it can enable copy-propagation by moving
56+
/// destroys above deinit barries and access scopes.
57+
///
58+
let destroyHoisting = FunctionPass(name: "destroy-hoisting") {
59+
(function: Function, context: FunctionPassContext) in
60+
61+
if !function.hasOwnership {
62+
return
63+
}
64+
65+
for block in function.blocks {
66+
for arg in block.arguments {
67+
optimize(value: arg, context)
68+
if !context.continueWithNextSubpassRun() {
69+
return
70+
}
71+
}
72+
for inst in block.instructions {
73+
for result in inst.results {
74+
optimize(value: result, context)
75+
if !context.continueWithNextSubpassRun(for: inst) {
76+
return
77+
}
78+
}
79+
}
80+
}
81+
}
82+
83+
private func optimize(value: Value, _ context: FunctionPassContext) {
84+
guard value.ownership == .owned,
85+
// Avoid all the analysis effort if there are no destroys to hoist.
86+
!value.uses.filterUsers(ofType: DestroyValueInst.self).isEmpty
87+
else {
88+
return
89+
}
90+
91+
var hoistableDestroys = selectHoistableDestroys(of: value, context)
92+
defer { hoistableDestroys.deinitialize() }
93+
94+
var minimalLiverange = InstructionRange(withLiverangeOf: value, ignoring: hoistableDestroys, context)
95+
defer { minimalLiverange.deinitialize() }
96+
97+
hoistDestroys(of: value, toEndOf: minimalLiverange, restrictingTo: &hoistableDestroys, context)
98+
}
99+
100+
private func selectHoistableDestroys(of value: Value, _ context: FunctionPassContext) -> InstructionSet {
101+
// Also includes liveranges of copied values and values stored to memory.
102+
var forwardExtendedLiverange = InstructionRange(withForwardExtendedLiverangeOf: value, context)
103+
defer { forwardExtendedLiverange.deinitialize() }
104+
105+
let deadEndBlocks = context.deadEndBlocks
106+
var hoistableDestroys = InstructionSet(context)
107+
108+
for use in value.uses {
109+
if let destroy = use.instruction as? DestroyValueInst,
110+
// We can hoist all destroys for which another copy of the value is alive at the destroy.
111+
forwardExtendedLiverange.contains(destroy),
112+
// TODO: once we have complete OSSA lifetimes we don't need to handle dead-end blocks.
113+
!deadEndBlocks.isDeadEnd(destroy.parentBlock)
114+
{
115+
hoistableDestroys.insert(destroy)
116+
}
117+
}
118+
return hoistableDestroys
119+
}
120+
121+
private func hoistDestroys(of value: Value,
122+
toEndOf minimalLiverange: InstructionRange,
123+
restrictingTo hoistableDestroys: inout InstructionSet,
124+
_ context: FunctionPassContext)
125+
{
126+
createNewDestroys(for: value, atEndPointsOf: minimalLiverange, reusing: &hoistableDestroys, context)
127+
128+
createNewDestroys(for: value, atExitPointsOf: minimalLiverange, reusing: &hoistableDestroys, context)
129+
130+
removeDestroys(of: value, restrictingTo: hoistableDestroys, context)
131+
}
132+
133+
private func createNewDestroys(
134+
for value: Value,
135+
atEndPointsOf liverange: InstructionRange,
136+
reusing hoistableDestroys: inout InstructionSet,
137+
_ context: FunctionPassContext
138+
) {
139+
let deadEndBlocks = context.deadEndBlocks
140+
141+
for endInst in liverange.ends {
142+
if !endInst.endsLifetime(of: value) {
143+
Builder.insert(after: endInst, context) { builder in
144+
builder.createDestroy(of: value, reusing: &hoistableDestroys, notIn: deadEndBlocks)
145+
}
146+
}
147+
}
148+
}
149+
150+
private func createNewDestroys(
151+
for value: Value,
152+
atExitPointsOf liverange: InstructionRange,
153+
reusing hoistableDestroys: inout InstructionSet,
154+
_ context: FunctionPassContext
155+
) {
156+
let deadEndBlocks = context.deadEndBlocks
157+
158+
for exitBlock in liverange.exitBlocks {
159+
let builder = Builder(atBeginOf: exitBlock, context)
160+
builder.createDestroy(of: value, reusing: &hoistableDestroys, notIn: deadEndBlocks)
161+
}
162+
}
163+
164+
private func removeDestroys(
165+
of value: Value,
166+
restrictingTo hoistableDestroys: InstructionSet,
167+
_ context: FunctionPassContext
168+
) {
169+
for use in value.uses {
170+
if let destroy = use.instruction as? DestroyValueInst,
171+
hoistableDestroys.contains(destroy)
172+
{
173+
context.erase(instruction: destroy)
174+
}
175+
}
176+
}
177+
178+
private extension InstructionRange {
179+
180+
init(withLiverangeOf initialDef: Value, ignoring ignoreDestroys: InstructionSet, _ context: FunctionPassContext)
181+
{
182+
var liverange = InstructionRange(for: initialDef, context)
183+
var visitor = InteriorUseWalker(definingValue: initialDef, ignoreEscape: true, visitInnerUses: false, context) {
184+
if !ignoreDestroys.contains($0.instruction) {
185+
liverange.insert($0.instruction)
186+
}
187+
return .continueWalk
188+
}
189+
defer { visitor.deinitialize() }
190+
191+
_ = visitor.visitUses()
192+
self = liverange
193+
}
194+
195+
// In addition to the forward-extended liverange, also follows copy_value's transitively.
196+
init(withForwardExtendedLiverangeOf initialDef: Value, _ context: FunctionPassContext) {
197+
self.init(for: initialDef, context)
198+
199+
var worklist = ValueWorklist(context)
200+
defer { worklist.deinitialize() }
201+
202+
worklist.pushIfNotVisited(initialDef)
203+
while let value = worklist.pop() {
204+
assert(value.ownership == .owned)
205+
206+
for use in value.uses {
207+
let user = use.instruction
208+
if !use.endsLifetime {
209+
if let copy = user as? CopyValueInst {
210+
worklist.pushIfNotVisited(copy)
211+
}
212+
continue
213+
}
214+
215+
switch user {
216+
case let store as StoreInst:
217+
extendLiverangeInMemory(of: initialDef, with: store, context)
218+
219+
case let termInst as TermInst & ForwardingInstruction:
220+
worklist.pushIfNotVisited(contentsOf: termInst.forwardedResults.lazy.filter({ $0.ownership != .none }))
221+
222+
case is ForwardingInstruction, is MoveValueInst:
223+
if let result = user.results.lazy.filter({ $0.ownership != .none }).singleElement {
224+
worklist.pushIfNotVisited(result)
225+
}
226+
227+
default:
228+
// We cannot extend a lexical liverange with a non-lexical liverange, because afterwards the
229+
// non-lexical liverange could be shrunk over a deinit barrier which would let the original
230+
// lexical liverange to be shrunk, too.
231+
if !initialDef.isInLexicalLiverange(context) || value.isInLexicalLiverange(context) {
232+
self.insert(user)
233+
}
234+
}
235+
}
236+
}
237+
}
238+
239+
private mutating func extendLiverangeInMemory(
240+
of initialDef: Value,
241+
with store: StoreInst,
242+
_ context: FunctionPassContext
243+
) {
244+
let domTree = context.dominatorTree
245+
246+
if initialDef.destroyUsers(dominatedBy: store.parentBlock, domTree).isEmpty {
247+
return
248+
}
249+
250+
// We have to take care of lexical lifetimes. See comment above.
251+
if initialDef.isInLexicalLiverange(context) &&
252+
!store.destination.accessBase.isInLexicalOrGlobalLiverange(context)
253+
{
254+
return
255+
}
256+
257+
if isTakeOrDestroy(ofAddress: store.destination, after: store, beforeDestroysOf: initialDef, context) {
258+
return
259+
}
260+
261+
self.insert(contentsOf: initialDef.destroyUsers(dominatedBy: store.parentBlock, domTree).map { $0.next! })
262+
}
263+
}
264+
265+
private func isTakeOrDestroy(
266+
ofAddress address: Value,
267+
after store: StoreInst,
268+
beforeDestroysOf initialDef: Value,
269+
_ context: FunctionPassContext
270+
) -> Bool {
271+
let aliasAnalysis = context.aliasAnalysis
272+
let domTree = context.dominatorTree
273+
var worklist = InstructionWorklist(context)
274+
defer { worklist.deinitialize() }
275+
276+
worklist.pushIfNotVisited(store.next!)
277+
while let inst = worklist.pop() {
278+
if inst.endsLifetime(of: initialDef) {
279+
continue
280+
}
281+
if inst.mayTakeOrDestroy(address: address, aliasAnalysis) {
282+
return true
283+
}
284+
if let next = inst.next {
285+
worklist.pushIfNotVisited(next)
286+
} else {
287+
for succ in inst.parentBlock.successors where store.parentBlock.dominates(succ, domTree) {
288+
worklist.pushIfNotVisited(succ.instructions.first!)
289+
}
290+
}
291+
}
292+
return false
293+
}
294+
295+
private extension Builder {
296+
func createDestroy(of value: Value,
297+
reusing hoistableDestroys: inout InstructionSet,
298+
notIn deadEndBlocks: DeadEndBlocksAnalysis) {
299+
guard case .before(let insertionPoint) = insertionPoint else {
300+
fatalError("unexpected kind of insertion point")
301+
}
302+
if deadEndBlocks.isDeadEnd(insertionPoint.parentBlock) {
303+
return
304+
}
305+
if hoistableDestroys.contains(insertionPoint) {
306+
hoistableDestroys.erase(insertionPoint)
307+
} else {
308+
createDestroyValue(operand: value)
309+
}
310+
}
311+
}
312+
313+
private extension Value {
314+
func destroyUsers(dominatedBy domBlock: BasicBlock, _ domTree: DominatorTree) ->
315+
LazyMapSequence<LazyFilterSequence<LazyMapSequence<UseList, DestroyValueInst?>>, DestroyValueInst> {
316+
return uses.lazy.compactMap { use in
317+
if let destroy = use.instruction as? DestroyValueInst,
318+
domBlock.dominates(destroy.parentBlock, domTree)
319+
{
320+
return destroy
321+
}
322+
return nil
323+
}
324+
}
325+
}
326+
327+
private extension Instruction {
328+
func endsLifetime(of value: Value) -> Bool {
329+
return operands.contains { $0.value == value && $0.endsLifetime }
330+
}
331+
332+
func mayTakeOrDestroy(address: Value, _ aliasAnalysis: AliasAnalysis) -> Bool {
333+
switch self {
334+
case is BeginAccessInst, is EndAccessInst, is EndBorrowInst:
335+
return false
336+
default:
337+
return mayWrite(toAddress: address, aliasAnalysis)
338+
}
339+
}
340+
}
341+
342+
private extension AccessBase {
343+
func isInLexicalOrGlobalLiverange(_ context: FunctionPassContext) -> Bool {
344+
switch self {
345+
case .box(let pbi): return pbi.box.isInLexicalLiverange(context)
346+
case .class(let rea): return rea.instance.isInLexicalLiverange(context)
347+
case .tail(let rta): return rta.instance.isInLexicalLiverange(context)
348+
case .stack(let asi): return asi.isLexical
349+
case .global: return true
350+
case .argument(let arg):
351+
switch arg.convention {
352+
case .indirectIn, .indirectInGuaranteed, .indirectInout, .indirectInoutAliasable:
353+
return arg.isLexical
354+
default:
355+
return false
356+
}
357+
case .yield, .storeBorrow, .pointer, .index, .unidentified:
358+
return false
359+
}
360+
}
361+
}

SwiftCompilerSources/Sources/Optimizer/PassManager/PassRegistration.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ private func registerSwiftPasses() {
7575
registerPass(mergeCondFailsPass, { mergeCondFailsPass.run($0) })
7676
registerPass(computeEscapeEffects, { computeEscapeEffects.run($0) })
7777
registerPass(computeSideEffects, { computeSideEffects.run($0) })
78+
registerPass(destroyHoisting, { destroyHoisting.run($0) })
7879
registerPass(initializeStaticGlobalsPass, { initializeStaticGlobalsPass.run($0) })
7980
registerPass(objCBridgingOptimization, { objCBridgingOptimization.run($0) })
8081
registerPass(objectOutliner, { objectOutliner.run($0) })

0 commit comments

Comments
 (0)