Skip to content

Commit a33ff98

Browse files
authored
Merge pull request #81969 from eeckstein/temp-lvalue-elimination
Optimizer: improve TempLValueOpt
2 parents 98aeec6 + 2b9b2d2 commit a33ff98

File tree

22 files changed

+692
-446
lines changed

22 files changed

+692
-446
lines changed

SwiftCompilerSources/Sources/Optimizer/Analysis/AliasAnalysis.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,8 @@ struct AliasAnalysis {
327327
return defaultEffects(of: endBorrow, on: memLoc)
328328

329329
case let debugValue as DebugValueInst:
330-
if debugValue.operand.value.type.isAddress && memLoc.mayAlias(with: debugValue.operand.value, self) {
330+
let v = debugValue.operand.value
331+
if v.type.isAddress, !(v is Undef), memLoc.mayAlias(with: v, self) {
331332
return .init(read: true)
332333
} else {
333334
return .noEffects
@@ -434,6 +435,11 @@ struct AliasAnalysis {
434435
}
435436
let callee = builtin.operands[1].value
436437
return context.calleeAnalysis.getSideEffects(ofCallee: callee).memory
438+
case .PrepareInitialization, .ZeroInitializer:
439+
if builtin.arguments.count == 1, memLoc.mayAlias(with: builtin.arguments[0], self) {
440+
return .init(write: true)
441+
}
442+
return .noEffects
437443
default:
438444
return defaultEffects(of: builtin, on: memLoc)
439445
}

SwiftCompilerSources/Sources/Optimizer/DataStructures/Set.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,14 @@ struct OperandSet : IntrusiveSet {
268268
}
269269
}
270270

271+
extension InstructionSet {
272+
mutating func insert<I: Instruction>(contentsOf source: some Sequence<I>) {
273+
for inst in source {
274+
_ = insert(inst)
275+
}
276+
}
277+
}
278+
271279
extension IntrusiveSet {
272280
mutating func insert(contentsOf source: some Sequence<Element>) {
273281
for element in source {

SwiftCompilerSources/Sources/Optimizer/DataStructures/Worklist.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,5 +93,20 @@ extension InstructionWorklist {
9393
}
9494
}
9595
}
96+
97+
mutating func pushSuccessors(of inst: Instruction, ignoring ignoreInst: Instruction) {
98+
if let succ = inst.next {
99+
if succ != ignoreInst {
100+
pushIfNotVisited(succ)
101+
}
102+
} else {
103+
for succBlock in inst.parentBlock.successors {
104+
let firstInst = succBlock.instructions.first!
105+
if firstInst != ignoreInst {
106+
pushIfNotVisited(firstInst)
107+
}
108+
}
109+
}
110+
}
96111
}
97112

SwiftCompilerSources/Sources/Optimizer/FunctionPasses/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,6 @@ swift_compiler_sources(Optimizer
3333
SimplificationPasses.swift
3434
StackPromotion.swift
3535
StripObjectHeaders.swift
36+
TempLValueElimination.swift
3637
TempRValueElimination.swift
3738
)
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
//===--- TempLValueElimination.swift ---------------------------------------==//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2025 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 AST
14+
import SIL
15+
16+
/// Eliminates copies from a temporary (an "l-value") to a destination.
17+
///
18+
/// ```
19+
/// %temp = alloc_stack $T
20+
/// ... -+
21+
/// store %x to %temp | no reads or writes to %destination
22+
/// ... -+
23+
/// copy_addr [take] %temp to [init] %destination
24+
/// dealloc_stack %temp
25+
/// ```
26+
/// ->
27+
/// ```
28+
/// ...
29+
/// store %x to %destination
30+
/// ...
31+
/// ```
32+
///
33+
/// The name TempLValueElimination refers to the TempRValueElimination pass, which performs
34+
/// a related transformation, just with the temporary on the "right" side.
35+
///
36+
/// The pass also performs a peephole optimization on `copy_addr` - `destroy_addr` sequences.
37+
/// It replaces
38+
///
39+
/// ```
40+
/// copy_addr %source to %destination
41+
/// destroy_addr %source
42+
/// ```
43+
/// ->
44+
/// ```
45+
/// copy_addr [take] %source to %destination
46+
/// ```
47+
///
48+
let tempLValueElimination = FunctionPass(name: "temp-lvalue-elimination") {
49+
(function: Function, context: FunctionPassContext) in
50+
51+
for inst in function.instructions {
52+
switch inst {
53+
case let copy as CopyAddrInst:
54+
combineWithDestroy(copy: copy, context)
55+
tryEliminate(copy: copy, context)
56+
case let store as StoreInst:
57+
// Also handle `load`-`store` pairs which are basically the same thing as a `copy_addr`.
58+
if let load = store.source as? LoadInst, load.uses.isSingleUse, load.parentBlock == store.parentBlock {
59+
tryEliminate(copy: store, context)
60+
}
61+
default:
62+
break
63+
}
64+
}
65+
}
66+
67+
private func tryEliminate(copy: CopyLikeInstruction, _ context: FunctionPassContext) {
68+
guard let allocStack = copy.sourceAddress as? AllocStackInst,
69+
allocStack.isDeallocatedInSameBlock(as: copy)
70+
else {
71+
return
72+
}
73+
let isTrivial = allocStack.type.isTrivial(in: copy.parentFunction)
74+
guard copy.isTakeOfSource || isTrivial else {
75+
return
76+
}
77+
78+
// We need to move all destination address projections at the begin of the alloc_stack liverange,
79+
// because we are replacing the alloc_stack uses with the destination.
80+
// ```
81+
// %destination = struct_element_addr %1
82+
// stores to %temp --> stores to %destination
83+
// %destination = struct_element_addr %1
84+
// copy_addr [take] %temp to %destination
85+
// ```
86+
var projections = InstructionSet(context)
87+
defer { projections.deinitialize() }
88+
let destinationRootAddress = collectMovableProjections(of: copy.destinationAddress, in: &projections)
89+
90+
// If true we need to explicitly destroy the destination at the begin of the liverange.
91+
// ```
92+
// destroy_addr %destination
93+
// stores to %temp --> stores to %destination
94+
// copy_addr [take] %temp to %destination
95+
// ```
96+
let needDestroyEarly = !copy.isInitializationOfDestination && !isTrivial
97+
98+
let firstUseOfAllocStack = InstructionList(first: allocStack).first(where: { $0.isUsing(allocStack) }) ??
99+
// The conservative default, if the fist use is not in the alloc_stack's block.
100+
allocStack.parentBlock.terminator
101+
102+
if firstUseOfAllocStack == copy.loadingInstruction {
103+
// The alloc_stack is not written yet at the point of the copy. This is a very unusual corner case
104+
// which can only happen if the alloc_stack has an empty type (e.g. `$()`).
105+
return
106+
}
107+
108+
let aliasAnalysis = context.aliasAnalysis
109+
let calleeAnalysis = context.calleeAnalysis
110+
111+
if aliasAnalysis.mayAlias(allocStack, copy.destinationAddress) {
112+
// Catch the very unusual corner case where the copy is writing back to it's source address - the alloc_stack.
113+
return
114+
}
115+
116+
var worklist = InstructionWorklist(context)
117+
defer { worklist.deinitialize() }
118+
worklist.pushIfNotVisited(firstUseOfAllocStack)
119+
120+
// Check instructions within the liverange of the alloc_stack.
121+
while let inst = worklist.pop() {
122+
// If the destination root address is within the liverange it would prevent moving the projections
123+
// before the first use. Note that if the defining instruction of `destinationRootAddress` is nil
124+
// it can only be a function argument.
125+
if inst == destinationRootAddress.definingInstruction {
126+
return
127+
}
128+
129+
// Check if the destination is not accessed within the liverange of the temporary.
130+
// This is unlikely, because the destination is initialized at the copy.
131+
// But still, the destination could contain an initialized value which is destroyed before the copy.
132+
if inst.mayReadOrWrite(address: copy.destinationAddress, aliasAnalysis) &&
133+
// Needed to treat `init_existential_addr` as not-writing projection.
134+
!projections.contains(inst)
135+
{
136+
return
137+
}
138+
139+
// Check if replacing the alloc_stack with destination would invalidate the alias rules of indirect arguments.
140+
if let apply = inst as? FullApplySite,
141+
apply.hasInvalidArgumentAliasing(between: allocStack, and: copy.destinationAddress, aliasAnalysis)
142+
{
143+
return
144+
}
145+
146+
// We must not shrink the liverange of an existing value in the destination.
147+
if needDestroyEarly && inst.isDeinitBarrier(calleeAnalysis) {
148+
return
149+
}
150+
151+
worklist.pushSuccessors(of: inst, ignoring: copy)
152+
}
153+
154+
if allocStack.isReadOrWritten(after: copy.loadingInstruction, aliasAnalysis) {
155+
// Bail in the unlikely case of the alloc_stack is re-initialized after its value has been taken by `copy`.
156+
return
157+
}
158+
159+
moveProjections(of: copy.destinationAddress, within: worklist, before: firstUseOfAllocStack, context)
160+
161+
if needDestroyEarly {
162+
// Make sure the destination is uninitialized before the liverange of the temporary.
163+
let builder = Builder(before: firstUseOfAllocStack, context)
164+
builder.createDestroyAddr(address: copy.destinationAddress)
165+
}
166+
167+
// Replace all uses of the temporary with the destination address.
168+
for use in allocStack.uses {
169+
switch use.instruction {
170+
case let deallocStack as DeallocStackInst:
171+
context.erase(instruction: deallocStack)
172+
default:
173+
use.set(to: copy.destinationAddress, context)
174+
}
175+
}
176+
context.erase(instruction: allocStack)
177+
context.erase(instructionIncludingAllUsers: copy.loadingInstruction)
178+
}
179+
180+
private extension FullApplySite {
181+
/// Returns true if after replacing `addr1` with `addr2` the apply would have invalid aliasing of
182+
/// indirect arguments.
183+
/// An indirect argument (except `@inout_aliasable`) must not alias with another indirect argument.
184+
/// For example, if we would replace `addr1` with `addr2` in
185+
/// ```
186+
/// apply %f(%addr1, %addr2) : (@in T) -> @out T
187+
/// ```
188+
/// we would invalidate this rule.
189+
func hasInvalidArgumentAliasing(between addr1: Value, and addr2: Value, _ aliasAnalysis: AliasAnalysis) -> Bool {
190+
var addr1Accessed = false
191+
var addr2Accessed = false
192+
var mutatingAccess = false
193+
for argOp in argumentOperands {
194+
let convention = convention(of: argOp)!
195+
if convention.isExclusiveIndirect {
196+
if aliasAnalysis.mayAlias(addr1, argOp.value) {
197+
addr1Accessed = true
198+
if !convention.isGuaranteed {
199+
mutatingAccess = true
200+
}
201+
} else if aliasAnalysis.mayAlias(addr2, argOp.value) {
202+
addr2Accessed = true
203+
if !convention.isGuaranteed {
204+
mutatingAccess = true
205+
}
206+
}
207+
}
208+
}
209+
return mutatingAccess && addr1Accessed && addr2Accessed
210+
}
211+
}
212+
213+
/// Replace
214+
/// ```
215+
/// copy_addr %source to %destination --> copy_addr [take] %source to %destination
216+
/// destroy_addr %source
217+
/// ```
218+
private func combineWithDestroy(copy: CopyAddrInst, _ context: FunctionPassContext) {
219+
guard !copy.isTakeOfSource,
220+
let destroy = copy.source.uses.users(ofType: DestroyAddrInst.self).first,
221+
destroy.parentBlock == copy.parentBlock
222+
else {
223+
return
224+
}
225+
226+
// Check if the destroy_addr is after the copy_addr and if there are no memory accesses between them.
227+
var debugInsts = Stack<DebugValueInst>(context)
228+
defer { debugInsts.deinitialize() }
229+
230+
for inst in InstructionList(first: copy.next) {
231+
if inst == destroy {
232+
break
233+
}
234+
if let debugInst = inst as? DebugValueInst, debugInst.operand.value == copy.source {
235+
debugInsts.append(debugInst)
236+
}
237+
if inst.mayReadOrWriteMemory {
238+
return
239+
}
240+
}
241+
copy.set(isTakeOfSource: true, context)
242+
context.erase(instruction: destroy)
243+
// Don't let debug info think that the value is still valid after the `copy [take]`.
244+
context.erase(instructions: debugInsts)
245+
}
246+
247+
private extension Value {
248+
var isMovableProjection: (SingleValueInstruction & UnaryInstruction)? {
249+
switch self {
250+
case let projectionInst as InitEnumDataAddrInst: return projectionInst
251+
case let projectionInst as StructElementAddrInst: return projectionInst
252+
case let projectionInst as TupleElementAddrInst: return projectionInst
253+
case let projectionInst as UncheckedTakeEnumDataAddrInst: return projectionInst
254+
case let projectionInst as InitExistentialAddrInst: return projectionInst
255+
case let projectionInst as RefElementAddrInst: return projectionInst
256+
case let projectionInst as RefTailAddrInst: return projectionInst
257+
case let projectionInst as ProjectBoxInst: return projectionInst
258+
default: return nil
259+
}
260+
}
261+
}
262+
263+
private func collectMovableProjections(of address: Value, in projections: inout InstructionSet) -> Value {
264+
var a = address
265+
while let projection = a.isMovableProjection {
266+
projections.insert(projection)
267+
a = projection.operand.value
268+
}
269+
return a
270+
}
271+
272+
private func moveProjections(
273+
of address: Value,
274+
within worklist: InstructionWorklist,
275+
before insertionPoint: Instruction,
276+
_ context: FunctionPassContext
277+
) {
278+
var a = address
279+
var ip = insertionPoint
280+
while let projection = a.isMovableProjection,
281+
worklist.hasBeenPushed(projection)
282+
{
283+
projection.move(before: ip, context)
284+
a = projection.operand.value
285+
ip = projection
286+
}
287+
}
288+
289+
private extension AllocStackInst {
290+
func isReadOrWritten(after afterInst: Instruction, _ aliasAnalysis: AliasAnalysis) -> Bool {
291+
for inst in InstructionList(first: afterInst.next) {
292+
if let deallocStack = inst as? DeallocStackInst, deallocStack.allocatedValue == self {
293+
return false
294+
}
295+
if inst.mayReadOrWrite(address: self, aliasAnalysis) {
296+
return true
297+
}
298+
}
299+
fatalError("dealloc_stack expected to be in same block as `afterInst`")
300+
}
301+
302+
func isDeallocatedInSameBlock(as inst: Instruction) -> Bool {
303+
if let deallocStack = uses.users(ofType: DeallocStackInst.self).singleElement,
304+
deallocStack.parentBlock == inst.parentBlock
305+
{
306+
return true
307+
}
308+
return false
309+
}
310+
}

0 commit comments

Comments
 (0)