Skip to content

Commit e5eb15d

Browse files
committed
Swift SIL: replace the set_deallocating instruction with begin_dealloc_ref
Codegen is the same, but `begin_dealloc_ref` consumes the operand and produces a new SSA value. This cleanly splits the liferange to the region before and within the destructor of a class.
1 parent d457368 commit e5eb15d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+444
-269
lines changed

SwiftCompilerSources/Sources/Optimizer/FunctionPasses/ObjectOutliner.swift

+51-33
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ private func optimizeObjectAllocation(allocRef: AllocRefInstBase, _ context: Fun
6363
return nil
6464
}
6565

66-
guard let (storesToClassFields, storesToTailElements) = getInitialization(of: allocRef, ignore: endCOW) else {
66+
guard let (storesToClassFields, storesToTailElements) = getInitialization(of: allocRef) else {
6767
return nil
6868
}
6969

@@ -75,7 +75,7 @@ private func optimizeObjectAllocation(allocRef: AllocRefInstBase, _ context: Fun
7575
context.erase(instructions: storesToClassFields)
7676
context.erase(instructions: storesToTailElements)
7777

78-
return replace(object: allocRef, with: outlinedGlobal, endCOW, context)
78+
return replace(object: allocRef, with: outlinedGlobal, context)
7979
}
8080

8181
private func findEndCOWMutation(of object: Value) -> EndCOWMutationInst? {
@@ -98,9 +98,8 @@ private func findEndCOWMutation(of object: Value) -> EndCOWMutationInst? {
9898
return nil
9999
}
100100

101-
private func getInitialization(of allocRef: AllocRefInstBase,
102-
ignore endCOW: EndCOWMutationInst) -> (storesToClassFields: [StoreInst],
103-
storesToTailElements: [StoreInst])? {
101+
private func getInitialization(of allocRef: AllocRefInstBase) -> (storesToClassFields: [StoreInst],
102+
storesToTailElements: [StoreInst])? {
104103
guard let numTailElements = allocRef.numTailElements else {
105104
return nil
106105
}
@@ -115,7 +114,7 @@ private func getInitialization(of allocRef: AllocRefInstBase,
115114
// store %1 to %4
116115
var tailStores = Array<StoreInst?>(repeating: nil, count: numTailElements * allocRef.numStoresPerTailElement)
117116

118-
if !findInitStores(of: allocRef, &fieldStores, &tailStores, ignore: endCOW) {
117+
if !findInitStores(of: allocRef, &fieldStores, &tailStores) {
119118
return nil
120119
}
121120

@@ -128,16 +127,15 @@ private func getInitialization(of allocRef: AllocRefInstBase,
128127

129128
private func findInitStores(of object: Value,
130129
_ fieldStores: inout [StoreInst?],
131-
_ tailStores: inout [StoreInst?],
132-
ignore endCOW: EndCOWMutationInst) -> Bool {
130+
_ tailStores: inout [StoreInst?]) -> Bool {
133131
for use in object.uses {
134132
switch use.instruction {
135133
case let uci as UpcastInst:
136-
if !findInitStores(of: uci, &fieldStores, &tailStores, ignore: endCOW) {
134+
if !findInitStores(of: uci, &fieldStores, &tailStores) {
137135
return false
138136
}
139137
case let mvi as MoveValueInst:
140-
if !findInitStores(of: mvi, &fieldStores, &tailStores, ignore: endCOW) {
138+
if !findInitStores(of: mvi, &fieldStores, &tailStores) {
141139
return false
142140
}
143141
case let rea as RefElementAddrInst:
@@ -149,7 +147,7 @@ private func findInitStores(of object: Value,
149147
return false
150148
}
151149
default:
152-
if !isValidUseOfObject(use.instruction, ignore: endCOW) {
150+
if !isValidUseOfObject(use.instruction) {
153151
return false
154152
}
155153
}
@@ -216,11 +214,7 @@ private func handleStore(_ store: StoreInst, index: Int, stores: inout [StoreIns
216214
return false
217215
}
218216

219-
private func isValidUseOfObject(_ inst: Instruction, ignore endCOW: EndCOWMutationInst? = nil) -> Bool {
220-
if inst == endCOW {
221-
return true
222-
}
223-
217+
private func isValidUseOfObject(_ inst: Instruction) -> Bool {
224218
switch inst {
225219
case is DebugValueInst,
226220
is LoadInst,
@@ -229,7 +223,7 @@ private func isValidUseOfObject(_ inst: Instruction, ignore endCOW: EndCOWMutati
229223
is StrongRetainInst,
230224
is StrongReleaseInst,
231225
is FixLifetimeInst,
232-
is SetDeallocatingInst:
226+
is EndCOWMutationInst:
233227
return true
234228

235229
case is StructElementAddrInst,
@@ -240,9 +234,12 @@ private func isValidUseOfObject(_ inst: Instruction, ignore endCOW: EndCOWMutati
240234
is EnumInst,
241235
is StructExtractInst,
242236
is UncheckedRefCastInst,
243-
is UpcastInst:
237+
is UpcastInst,
238+
is BeginDeallocRefInst,
239+
is RefTailAddrInst,
240+
is RefElementAddrInst:
244241
for use in (inst as! SingleValueInstruction).uses {
245-
if !isValidUseOfObject(use.instruction, ignore: endCOW) {
242+
if !isValidUseOfObject(use.instruction) {
246243
return false
247244
}
248245
}
@@ -312,31 +309,52 @@ private func constructObject(of allocRef: AllocRefInstBase,
312309

313310
private func replace(object allocRef: AllocRefInstBase,
314311
with global: GlobalVariable,
315-
_ endCOW: EndCOWMutationInst, _ context: FunctionPassContext) -> GlobalValueInst {
312+
_ context: FunctionPassContext) -> GlobalValueInst {
316313

317314
// Replace the alloc_ref by global_value + strong_retain instructions.
318315
let builder = Builder(before: allocRef, context)
319316
let globalValue = builder.createGlobalValue(global: global, isBare: false)
320317
builder.createStrongRetain(operand: globalValue)
321318

322-
endCOW.uses.replaceAll(with: endCOW.instance, context)
323-
context.erase(instruction: endCOW)
319+
rewriteUses(of: allocRef, context)
320+
allocRef.uses.replaceAll(with: globalValue, context)
321+
context.erase(instruction: allocRef)
322+
return globalValue
323+
}
324324

325-
for use in allocRef.uses {
326-
let user = use.instruction
327-
switch user {
328-
case is SetDeallocatingInst:
329-
let builder = Builder(before: user, context)
330-
builder.createStrongRelease(operand: globalValue)
331-
context.erase(instruction: user)
325+
private func rewriteUses(of startValue: Value, _ context: FunctionPassContext) {
326+
var worklist = InstructionWorklist(context)
327+
defer { worklist.deinitialize() }
328+
worklist.pushIfNotVisited(usersOf: startValue)
329+
330+
while let inst = worklist.pop() {
331+
switch inst {
332+
case let beginDealloc as BeginDeallocRefInst:
333+
worklist.pushIfNotVisited(usersOf: beginDealloc)
334+
let builder = Builder(before: beginDealloc, context)
335+
builder.createStrongRelease(operand: beginDealloc.reference)
336+
beginDealloc.uses.replaceAll(with: beginDealloc.reference, context)
337+
context.erase(instruction: beginDealloc)
338+
case let endMutation as EndCOWMutationInst:
339+
worklist.pushIfNotVisited(usersOf: endMutation)
340+
endMutation.uses.replaceAll(with: endMutation.instance, context)
341+
context.erase(instruction: endMutation)
342+
case let upCast as UpcastInst:
343+
worklist.pushIfNotVisited(usersOf: upCast)
344+
case let moveValue as MoveValueInst:
345+
worklist.pushIfNotVisited(usersOf: moveValue)
332346
case is DeallocRefInst, is DeallocStackRefInst:
333-
context.erase(instruction: user)
347+
context.erase(instruction: inst)
334348
default:
335-
use.set(to: globalValue, context)
349+
break
336350
}
337351
}
338-
context.erase(instruction: allocRef)
339-
return globalValue
352+
}
353+
354+
private extension InstructionWorklist {
355+
mutating func pushIfNotVisited(usersOf value: Value) {
356+
pushIfNotVisited(contentsOf: value.uses.lazy.map { $0.instruction })
357+
}
340358
}
341359

342360
private extension Value {

SwiftCompilerSources/Sources/Optimizer/FunctionPasses/ReleaseDevirtualizer.swift

+94-70
Original file line numberDiff line numberDiff line change
@@ -39,126 +39,150 @@ let releaseDevirtualizerPass = FunctionPass(name: "release-devirtualizer") {
3939
var lastRelease: RefCountingInst?
4040

4141
for instruction in block.instructions {
42-
if let release = lastRelease {
43-
// We only do the optimization for stack promoted object, because for
44-
// these we know that they don't have associated objects, which are
45-
// _not_ released by the deinit method.
46-
if let deallocStackRef = instruction as? DeallocStackRefInst {
47-
if !context.continueWithNextSubpassRun(for: release) {
42+
switch instruction {
43+
case let dealloc as DeallocStackRefInst:
44+
if let lastRel = lastRelease {
45+
// We only do the optimization for stack promoted object, because for
46+
// these we know that they don't have associated objects, which are
47+
// _not_ released by the deinit method.
48+
if !context.continueWithNextSubpassRun(for: lastRel) {
4849
return
4950
}
50-
tryDevirtualizeReleaseOfObject(context, release, deallocStackRef)
51+
tryDevirtualizeRelease(of: dealloc.allocRef, lastRelease: lastRel, context)
5152
lastRelease = nil
52-
continue
5353
}
54-
}
55-
56-
switch instruction {
57-
case is ReleaseValueInst, is StrongReleaseInst:
58-
lastRelease = instruction as? RefCountingInst
59-
case is DeallocRefInst, is SetDeallocatingInst:
54+
case let strongRelease as StrongReleaseInst:
55+
lastRelease = strongRelease
56+
case let releaseValue as ReleaseValueInst where releaseValue.value.type.containsSingleReference(in: function):
57+
lastRelease = releaseValue
58+
case is DeallocRefInst, is BeginDeallocRefInst:
59+
lastRelease = nil
60+
default:
61+
if instruction.mayRelease {
6062
lastRelease = nil
61-
default:
62-
if instruction.mayRelease {
63-
lastRelease = nil
64-
}
63+
}
6564
}
6665
}
6766
}
6867
}
6968

7069
/// Tries to de-virtualize the final release of a stack-promoted object.
71-
private func tryDevirtualizeReleaseOfObject(
72-
_ context: FunctionPassContext,
73-
_ release: RefCountingInst,
74-
_ deallocStackRef: DeallocStackRefInst
70+
private func tryDevirtualizeRelease(
71+
of allocRef: AllocRefInstBase,
72+
lastRelease: RefCountingInst,
73+
_ context: FunctionPassContext
7574
) {
76-
let allocRefInstruction = deallocStackRef.allocRef
75+
var downWalker = FindReleaseWalker(release: lastRelease)
76+
guard let pathToRelease = downWalker.getPathToRelease(from: allocRef) else {
77+
return
78+
}
79+
80+
if !pathToRelease.isMaterializable {
81+
return
82+
}
7783

78-
// Check if the release instruction right before the `dealloc_stack_ref` really releases
79-
// the allocated object (and not something else).
80-
var finder = FindAllocationOfRelease(allocation: allocRefInstruction)
81-
if !finder.allocationIsRoot(of: release.operand.value) {
84+
var upWalker = FindAllocationWalker(allocation: allocRef)
85+
if upWalker.walkUp(value: lastRelease.operand.value, path: pathToRelease) == .abortWalk {
8286
return
8387
}
8488

85-
let type = allocRefInstruction.type
89+
let type = allocRef.type
8690

8791
guard let dealloc = context.calleeAnalysis.getDestructor(ofExactType: type) else {
8892
return
8993
}
9094

91-
let builder = Builder(before: release, location: release.location, context)
95+
let builder = Builder(before: lastRelease, location: lastRelease.location, context)
9296

93-
var object: Value = allocRefInstruction
97+
var object = lastRelease.operand.value.createProjection(path: pathToRelease, builder: builder)
9498
if object.type != type {
9599
object = builder.createUncheckedRefCast(from: object, to: type)
96100
}
97101

98102
// Do what a release would do before calling the deallocator: set the object
99103
// in deallocating state, which means set the RC_DEALLOCATING_FLAG flag.
100-
builder.createSetDeallocating(operand: object, isAtomic: release.isAtomic)
104+
let beginDealloc = builder.createBeginDeallocRef(reference: object, allocation: allocRef)
101105

102106
// Create the call to the destructor with the allocated object as self
103107
// argument.
104108
let functionRef = builder.createFunctionRef(dealloc)
105109

106110
let substitutionMap = context.getContextSubstitutionMap(for: type)
107-
builder.createApply(function: functionRef, substitutionMap, arguments: [object])
108-
context.erase(instruction: release)
111+
builder.createApply(function: functionRef, substitutionMap, arguments: [beginDealloc])
112+
context.erase(instruction: lastRelease)
113+
}
114+
115+
private struct FindReleaseWalker : ValueDefUseWalker {
116+
private let release: RefCountingInst
117+
private var result: SmallProjectionPath? = nil
118+
119+
var walkDownCache = WalkerCache<SmallProjectionPath>()
120+
121+
init(release: RefCountingInst) {
122+
self.release = release
123+
}
124+
125+
mutating func getPathToRelease(from allocRef: AllocRefInstBase) -> SmallProjectionPath? {
126+
if walkDownUses(ofValue: allocRef, path: SmallProjectionPath()) == .continueWalk {
127+
return result
128+
}
129+
return nil
130+
}
131+
132+
mutating func leafUse(value: Operand, path: SmallProjectionPath) -> WalkResult {
133+
if value.instruction == release {
134+
if let existingResult = result {
135+
result = existingResult.merge(with: path)
136+
} else {
137+
result = path
138+
}
139+
}
140+
return .continueWalk
141+
}
109142
}
110143

111144
// Up-walker to find the root of a release instruction.
112-
private struct FindAllocationOfRelease : ValueUseDefWalker {
145+
private struct FindAllocationWalker : ValueUseDefWalker {
113146
private let allocInst: AllocRefInstBase
114-
private var allocFound = false
115147

116-
var walkUpCache = WalkerCache<UnusedWalkingPath>()
148+
var walkUpCache = WalkerCache<SmallProjectionPath>()
117149

118150
init(allocation: AllocRefInstBase) { allocInst = allocation }
119151

120-
/// The top-level entry point: returns true if the root of `value` is the `allocInst`.
121-
mutating func allocationIsRoot(of value: Value) -> Bool {
122-
return walkUp(value: value, path: UnusedWalkingPath()) != .abortWalk &&
123-
allocFound
152+
mutating func rootDef(value: Value, path: SmallProjectionPath) -> WalkResult {
153+
return value == allocInst && path.isEmpty ? .continueWalk : .abortWalk
124154
}
155+
}
125156

126-
mutating func rootDef(value: Value, path: UnusedWalkingPath) -> WalkResult {
127-
if value == allocInst {
128-
allocFound = true
129-
return .continueWalk
157+
private extension Type {
158+
func containsSingleReference(in function: Function) -> Bool {
159+
if isClass {
160+
return true
161+
}
162+
if isStruct {
163+
return getNominalFields(in: function).containsSingleReference(in: function)
164+
} else if isTuple {
165+
return tupleElements.containsSingleReference(in: function)
166+
} else {
167+
return false
130168
}
131-
return .abortWalk
132169
}
170+
}
133171

134-
// This function is called for `struct` and `tuple` instructions in case the `path` doesn't select
135-
// a specific operand but all operands.
136-
mutating func walkUpAllOperands(of def: Instruction, path: UnusedWalkingPath) -> WalkResult {
137-
138-
// Instead of walking up _all_ operands (which would be the default behavior), require that only a
139-
// _single_ operand is not trivial and walk up that operand.
140-
// We need to check this because the released value must not contain multiple copies of the
141-
// allocated object. We can only replace a _single_ release with the destructor call and not
142-
// multiple releases of the same object. E.g.
143-
//
144-
// %x = alloc_ref [stack] $X
145-
// strong_retain %x
146-
// %t = tuple (%x, %x)
147-
// release_value %t // -> releases %x two times!
148-
// dealloc_stack_ref %x
149-
//
150-
var nonTrivialOperandFound = false
151-
for operand in def.operands {
152-
if !operand.value.hasTrivialType {
153-
if nonTrivialOperandFound {
154-
return .abortWalk
172+
private extension Collection where Element == Type {
173+
func containsSingleReference(in function: Function) -> Bool {
174+
var nonTrivialFieldFound = false
175+
for elementTy in self {
176+
if !elementTy.isTrivial(in: function) {
177+
if nonTrivialFieldFound {
178+
return false
155179
}
156-
nonTrivialOperandFound = true
157-
if walkUp(value: operand.value, path: path) == .abortWalk {
158-
return .abortWalk
180+
if !elementTy.containsSingleReference(in: function) {
181+
return false
159182
}
183+
nonTrivialFieldFound = true
160184
}
161185
}
162-
return .continueWalk
186+
return nonTrivialFieldFound
163187
}
164188
}

0 commit comments

Comments
 (0)