Skip to content

Commit 2508360

Browse files
authored
Merge pull request #85027 from eeckstein/fix-temp-rvalue-elimination
TempRValueOptimization: respect deinit-barriers when hoisting destroys
2 parents 9069d32 + 53f424e commit 2508360

File tree

5 files changed

+449
-19
lines changed

5 files changed

+449
-19
lines changed

SwiftCompilerSources/Sources/Optimizer/FunctionPasses/TempRValueElimination.swift

Lines changed: 92 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,21 @@ private func removeTempRValues(in function: Function, keepDebugInfo: Bool, _ con
6969
private func tryEliminate(copy: CopyLikeInstruction, keepDebugInfo: Bool, _ context: FunctionPassContext) {
7070

7171
guard let (allocStack, lastUseOfAllocStack) =
72-
getRemovableAllocStackDestination(of: copy, keepDebugInfo: keepDebugInfo, context) else {
72+
getRemovableAllocStackDestination(of: copy, keepDebugInfo: keepDebugInfo, context),
73+
let needHoistDestroys = needHoistDestroys(of: allocStack, after: lastUseOfAllocStack, copy: copy, context)
74+
else {
7375
return
7476
}
7577

7678
guard extendAccessScopes(beyond: lastUseOfAllocStack, copy: copy, context) else {
7779
return
7880
}
7981

80-
if copy.isTakeOfSource {
82+
if needHoistDestroys {
83+
// Hoist destroy_addrs after the last use, because between the last use and the original destroy
84+
// the source is modified.
8185
hoistDestroy(of: allocStack, after: lastUseOfAllocStack, context)
82-
} else {
86+
} else if !copy.isTakeOfSource {
8387
removeDestroys(of: allocStack, context)
8488
}
8589

@@ -140,21 +144,6 @@ private func getRemovableAllocStackDestination(
140144
return nil
141145
}
142146

143-
// We cannot insert the destroy of `copy.source` after `lastUseOfAllocStack` if `copy.source` is
144-
// re-initialized by exactly this instruction.
145-
// This is a corner case, but can happen if `lastUseOfAllocStack` is a `copy_addr`. Example:
146-
// ```
147-
// copy_addr [take] %src to [init] %allocStack // `copy`
148-
// copy_addr [take] %allocStack to [init] %src // `lastUseOfAllocStack`
149-
// ```
150-
if copy.isTakeOfSource,
151-
lastUseOfAllocStack != copy,
152-
!(lastUseOfAllocStack is DestroyAddrInst),
153-
lastUseOfAllocStack.mayWrite(toAddress: copy.sourceAddress, context.aliasAnalysis)
154-
{
155-
return nil
156-
}
157-
158147
// Bail if in non-OSSA the `allocStack` is destroyed in a non-obvious way, e.g. by
159148
// ```
160149
// %x = load %allocStack // looks like a load, but is a `load [take]`
@@ -171,6 +160,91 @@ private func getRemovableAllocStackDestination(
171160
return (allocStack, lastUseOfAllocStack)
172161
}
173162

163+
/// Returns true if the final `destroy_addr`s need to be hoisted to the last use of the `allocStack`.
164+
/// This is required if the copy-source is re-initialized inbetween, e.g.
165+
/// ```
166+
/// copy_addr %source, %allocStack
167+
/// ...
168+
/// last_use(%allocStack)
169+
/// ... <- the destroy_addr needs to be moved here
170+
/// store %newValue to [init] %source
171+
/// ...
172+
/// destroy_addr %allocStack
173+
/// ```
174+
/// Returns nil if hoisting is needed but not possible.
175+
private func needHoistDestroys(of allocStack: AllocStackInst,
176+
after lastUseOfAllocStack: Instruction,
177+
copy: CopyLikeInstruction,
178+
_ context: FunctionPassContext
179+
) -> Bool? {
180+
guard copy.isTakeOfSource else {
181+
return false
182+
}
183+
if lastUseOfAllocStack is DestroyAddrInst {
184+
assert(!copy.parentFunction.hasOwnership, "should not treat destroy_addr as uses in OSSA")
185+
return false
186+
}
187+
188+
// We cannot insert the destroy of `copy.source` after `lastUseOfAllocStack` if `copy.source` is
189+
// re-initialized by exactly this instruction.
190+
// This is a corner case, but can happen if `lastUseOfAllocStack` is a `copy_addr`. Example:
191+
// ```
192+
// copy_addr [take] %src to [init] %allocStack // `copy`
193+
// copy_addr [take] %allocStack to [init] %src // `lastUseOfAllocStack`
194+
// ```
195+
if lastUseOfAllocStack != copy, lastUseOfAllocStack.mayWrite(toAddress: copy.sourceAddress, context.aliasAnalysis) {
196+
return nil
197+
}
198+
199+
if mayWrite(to: copy.sourceAddress, after: lastUseOfAllocStack, beforeDestroysOf: allocStack, context) {
200+
if hasDestroyBarrier(after: lastUseOfAllocStack, beforeDestroysOf: allocStack, context) {
201+
return nil
202+
}
203+
return true
204+
}
205+
return false
206+
}
207+
208+
private func mayWrite(to source: Value,
209+
after lastUse: Instruction,
210+
beforeDestroysOf allocStack: AllocStackInst,
211+
_ context: FunctionPassContext
212+
) -> Bool {
213+
return visitInstructions(after: lastUse, beforeDestroysOf: allocStack, context) { inst in
214+
inst.mayWrite(toAddress: source, context.aliasAnalysis)
215+
}
216+
}
217+
218+
private func hasDestroyBarrier(after lastUse: Instruction,
219+
beforeDestroysOf allocStack: AllocStackInst,
220+
_ context: FunctionPassContext
221+
) -> Bool {
222+
return visitInstructions(after: lastUse, beforeDestroysOf: allocStack, context) { inst in
223+
inst.isBarrierForDestroy(of: allocStack.type, context)
224+
}
225+
}
226+
227+
private func visitInstructions(after lastUse: Instruction,
228+
beforeDestroysOf allocStack: AllocStackInst,
229+
_ context: FunctionPassContext,
230+
_ predicate: (Instruction) -> Bool
231+
) -> Bool {
232+
var worklist = InstructionWorklist(context)
233+
defer { worklist.deinitialize() }
234+
235+
for destroy in allocStack.uses.users(ofType: DestroyAddrInst.self) {
236+
worklist.pushPredecessors(of: destroy, ignoring: lastUse)
237+
}
238+
239+
while let inst = worklist.pop() {
240+
if predicate(inst) {
241+
return true
242+
}
243+
worklist.pushPredecessors(of: inst, ignoring: lastUse)
244+
}
245+
return false
246+
}
247+
174248
// We need to hoist the destroy of the stack location up to its last use because the source of the copy
175249
// could be re-initialized between the last use and the destroy.
176250
private func hoistDestroy(of allocStack: AllocStackInst, after lastUse: Instruction, _ context: FunctionPassContext) {

SwiftCompilerSources/Sources/Optimizer/Utilities/FunctionTest.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ public func registerOptimizerTests() {
5151
localVariableReachableUsesTest,
5252
localVariableReachingAssignmentsTest,
5353
rangeOverlapsPathTest,
54-
variableIntroducerTest
54+
variableIntroducerTest,
55+
destroyBarrierTest
5556
)
5657

5758
// Finally register the thunk they all call through.

SwiftCompilerSources/Sources/Optimizer/Utilities/OptUtils.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,54 @@ extension Instruction {
546546
// Eventually we want to replace the SILCombine implementation with this one.
547547
return nil
548548
}
549+
550+
/// Returns true if a destroy of `type` must not be moved across this instruction.
551+
func isBarrierForDestroy(of type: Type, _ context: some Context) -> Bool {
552+
let instEffects = memoryEffects
553+
if instEffects == .noEffects || type.isTrivial(in: parentFunction) {
554+
return false
555+
}
556+
guard type.isMoveOnly else {
557+
// Non-trivial copyable types only have to consider deinit-barriers for classes.
558+
return isDeinitBarrier(context.calleeAnalysis)
559+
}
560+
561+
// Check side-effects of non-copyable deinits.
562+
563+
guard let nominal = type.nominal else {
564+
return true
565+
}
566+
if nominal.valueTypeDestructor != nil {
567+
guard let deinitFunc = context.lookupDeinit(ofNominal: nominal) else {
568+
return true
569+
}
570+
let destructorEffects = deinitFunc.getSideEffects().memory
571+
if instEffects.write || destructorEffects.write {
572+
return true
573+
}
574+
}
575+
576+
switch nominal {
577+
case is StructDecl:
578+
guard let fields = type.getNominalFields(in: parentFunction) else {
579+
return true
580+
}
581+
return fields.contains { isBarrierForDestroy(of: $0, context) }
582+
case is EnumDecl:
583+
guard let enumCases = type.getEnumCases(in: parentFunction) else {
584+
return true
585+
}
586+
return enumCases.contains {
587+
if let payload = $0.payload {
588+
return isBarrierForDestroy(of: payload, context)
589+
}
590+
return false
591+
}
592+
default:
593+
return true
594+
}
595+
}
596+
549597
}
550598

551599
// Match the pattern:
@@ -1133,3 +1181,14 @@ func cloneAndSpecializeFunction(from originalFunction: Function,
11331181
defer { cloner.deinitialize() }
11341182
cloner.cloneFunctionBody()
11351183
}
1184+
1185+
let destroyBarrierTest = FunctionTest("destroy_barrier") { function, arguments, context in
1186+
let type = arguments.takeValue().type
1187+
for inst in function.instructions {
1188+
if inst.isBarrierForDestroy(of: type, context) {
1189+
print("barrier: \(inst)")
1190+
} else {
1191+
print("transparent: \(inst)")
1192+
}
1193+
}
1194+
}

0 commit comments

Comments
 (0)