Skip to content

Commit 46c69e4

Browse files
authored
Merge pull request #85533 from eeckstein/fix-access-simplification
SILOptimizer: don't remove empty conflicting access scopes
2 parents c8b5df5 + 9a12474 commit 46c69e4

File tree

16 files changed

+801
-67
lines changed

16 files changed

+801
-67
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
ComputeEscapeEffects.swift
1818
ComputeSideEffects.swift
1919
CopyToBorrowOptimization.swift
20+
DeadAccessScopeElimination.swift
2021
DeadStoreElimination.swift
2122
DeinitDevirtualizer.swift
2223
DestroyHoisting.swift
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
//===--- DeadAccessScopeElimination.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 SIL
14+
15+
/// Eliminates dead access scopes if they are not conflicting with other scopes.
16+
///
17+
/// Removes:
18+
/// ```
19+
/// %2 = begin_access [modify] [dynamic] %1
20+
/// ... // no uses of %2
21+
/// end_access %2
22+
/// ```
23+
///
24+
/// However, dead _conflicting_ access scopes are not removed.
25+
/// If a conflicting scope becomes dead because an optimization e.g. removed a load, it is still
26+
/// important to get an access violation at runtime.
27+
/// Even a propagated value of a redundant load from a conflicting scope is undefined.
28+
///
29+
/// ```
30+
/// %2 = begin_access [modify] [dynamic] %1
31+
/// store %x to %2
32+
/// %3 = begin_access [read] [dynamic] %1 // conflicting with %2!
33+
/// %y = load %3
34+
/// end_access %3
35+
/// end_access %2
36+
/// use(%y)
37+
/// ```
38+
/// After redundant-load-elimination:
39+
/// ```
40+
/// %2 = begin_access [modify] [dynamic] %1
41+
/// store %x to %2
42+
/// %3 = begin_access [read] [dynamic] %1 // now dead, but still conflicting with %2
43+
/// end_access %3
44+
/// end_access %2
45+
/// use(%x) // propagated from the store, but undefined here!
46+
/// ```
47+
/// In this case the scope `%3` is not removed because it's important to get an access violation
48+
/// error at runtime before the undefined value `%x` is used.
49+
///
50+
/// This pass considers potential conflicting access scopes in called functions.
51+
/// But it does not consider potential conflicting access in callers (because it can't!).
52+
/// However, optimizations, like redundant-load-elimination, can only do such transformations if
53+
/// the outer access scope is within the function, e.g.
54+
///
55+
/// ```
56+
/// bb0(%0 : $*T): // an inout from a conflicting scope in the caller
57+
/// store %x to %0
58+
/// %3 = begin_access [read] [dynamic] %1
59+
/// %y = load %3 // cannot be propagated because it cannot be proved that %1 is the same address as %0
60+
/// end_access %3
61+
/// ```
62+
///
63+
/// All those checks are only done for dynamic access scopes, because they matter for runtime
64+
/// exclusivity checking. Dead static scopes are removed unconditionally.
65+
///
66+
let deadAccessScopeElimination = FunctionPass(name: "dead-access-scope-elimination") {
67+
(function: Function, context: FunctionPassContext) in
68+
69+
// Add all dead scopes here and then remove the ones which turn out to be conflicting.
70+
var removableScopes = SpecificIterableInstructionSet<BeginAccessInst>(context)
71+
defer { removableScopes.deinitialize() }
72+
73+
var scopeTree = ScopeTree(context)
74+
75+
// The payload is the recent access instruction at the block begin, e.g.
76+
// ```
77+
// %1 = begin_acces %0
78+
// br bb2
79+
// bb2: // recent access instruction at begin of bb2 is: `%1 = begin_acces %0`
80+
// ```
81+
// It's nil if the block is not within an access scope.
82+
//
83+
var blockWorklist = BasicBlockWorklistWithPayload<Instruction?>(context)
84+
defer { blockWorklist.deinitialize() }
85+
86+
blockWorklist.pushIfNotVisited(function.entryBlock, with: nil)
87+
88+
// Walk through the control flow in depth-first order. Note that we don't need to do any kind
89+
// of state merging at merge-points, because access scopes must be consistent on all paths.
90+
while let (block, recentAccessInstAtBlockBegin) = blockWorklist.pop() {
91+
92+
// The last seen `begin_access` (or `end_access` in case of not perfectly nested scopes; see ScopeTree.backlinks)
93+
var recentAccessInst = recentAccessInstAtBlockBegin
94+
95+
for inst in block.instructions {
96+
process(instruction: inst, updating: &recentAccessInst, &scopeTree, &removableScopes)
97+
}
98+
99+
blockWorklist.pushIfNotVisited(contentsOf: block.successors, with: recentAccessInst)
100+
}
101+
102+
for deadBeginAccess in removableScopes {
103+
context.erase(instructionIncludingAllUsers: deadBeginAccess)
104+
}
105+
}
106+
107+
private func process(instruction: Instruction,
108+
updating recentAccessInst: inout Instruction?,
109+
_ scopeTree: inout ScopeTree,
110+
_ removableScopes: inout SpecificIterableInstructionSet<BeginAccessInst>)
111+
{
112+
switch instruction {
113+
114+
case let beginAccess as BeginAccessInst:
115+
if beginAccess.isDead {
116+
// Might be removed again later if it turns out to be in a conflicting scope.
117+
removableScopes.insert(beginAccess)
118+
}
119+
if beginAccess.enforcement != .dynamic {
120+
// We unconditionally remove dead _static_ scopes, because they don't have any impact at runtime.
121+
// Usually static scopes are already removed in the optimization pipeline. However optimizations
122+
// might turn dynamic into static scopes. So let's handle them.
123+
break
124+
}
125+
scopeTree.visitEnclosingScopes(of: recentAccessInst) { enclosingBeginAccess in
126+
if beginAccess.accessKind.conflicts(with: enclosingBeginAccess.accessKind),
127+
// Avoid computing alias info if both scopes are not removable anyway.
128+
removableScopes.contains(beginAccess) || removableScopes.contains(enclosingBeginAccess),
129+
scopeTree.context.aliasAnalysis.mayAlias(beginAccess.address, enclosingBeginAccess.address)
130+
{
131+
// Conflicting enclosing scopes are not removable.
132+
removableScopes.erase(enclosingBeginAccess)
133+
// ... as well as the inner scope (which conflicts with the enclosing scope).
134+
removableScopes.erase(beginAccess)
135+
}
136+
}
137+
scopeTree.update(recent: &recentAccessInst, with: beginAccess)
138+
139+
case let endAccess as EndAccessInst where endAccess.beginAccess.enforcement == .dynamic:
140+
scopeTree.update(recent: &recentAccessInst, with: endAccess)
141+
142+
default:
143+
if instruction.mayCallFunction {
144+
// Check for potential conflicting scopes in called functions.
145+
scopeTree.visitEnclosingScopes(of: recentAccessInst) { enclosingBeginAccess in
146+
if removableScopes.contains(enclosingBeginAccess),
147+
instruction.mayHaveAccessScopeWhichConflicts(with: enclosingBeginAccess, scopeTree.context)
148+
{
149+
removableScopes.erase(enclosingBeginAccess)
150+
}
151+
}
152+
}
153+
}
154+
}
155+
156+
/// Represents the tree of access scopes in a function.
157+
/// Note that if the scopes are not nested perfectly, it's strictly speaking not a tree.
158+
private struct ScopeTree {
159+
160+
// Links `begin_access` and `end_access` instructions in backward control flow direction.
161+
// This is used to visit all enclosing scopes of a `begin_access`.
162+
// As an optimization, `end_access`es are ignored for scopes which are perfectly nested - which is
163+
// by far the most common case. In this case the backlinks simply are the parent links in the scope tree.
164+
//
165+
// Example of not perfectly nested scopes:
166+
// ```
167+
// %1 = begin_access <------------------+
168+
// ... |
169+
// %2 = begin_access <--------------+ -+
170+
// ... |
171+
// end_access %1 <---------+ -+
172+
// ... |
173+
// %3 = begin_access <-----+ -+
174+
// ... |
175+
// end_access %2 <-+ -+
176+
// ... |
177+
// end_access %3 -+
178+
// ```
179+
//
180+
// Perfectly nested scopes:
181+
// ```
182+
// %1 = begin_access <-+ <-+
183+
// ... | |
184+
// %2 = begin_access -+ |
185+
// end_access %2 | <- ignored
186+
// ... |
187+
// %3 = begin_access -------+
188+
// end_access %3 <- ignored
189+
// ...
190+
// end_access %1 <- ignored
191+
// ```
192+
private var backlinks = Dictionary<Instruction, Instruction>()
193+
194+
let context: FunctionPassContext
195+
196+
init(_ context: FunctionPassContext) { self.context = context }
197+
198+
mutating func update(recent: inout Instruction?, with beginAccess: BeginAccessInst) {
199+
backlinks[beginAccess] = recent
200+
recent = beginAccess
201+
}
202+
203+
mutating func update(recent: inout Instruction?, with endAccess: EndAccessInst) {
204+
if endAccess.beginAccess == recent {
205+
// The scope is perfectly nested. Ignore it and directly backlink to the parent of the `begin_access`
206+
recent = backlinks[endAccess.beginAccess]
207+
} else {
208+
backlinks[endAccess] = recent
209+
recent = endAccess
210+
}
211+
}
212+
213+
func visitEnclosingScopes(of accessInstruction: Instruction?, closure: (BeginAccessInst) -> ()) {
214+
// Ignore scopes which are already closed
215+
var ignore = SpecificInstructionSet<BeginAccessInst>(context)
216+
defer { ignore.deinitialize() }
217+
218+
var enclosingScope = accessInstruction
219+
220+
while let parent = enclosingScope {
221+
switch parent {
222+
case let parentBeginAccess as BeginAccessInst where !ignore.contains(parentBeginAccess):
223+
closure(parentBeginAccess)
224+
case let parentEndAccess as EndAccessInst:
225+
// Seeing an `end_access` in the backlink chain means that this scope is already closed.
226+
ignore.insert(parentEndAccess.beginAccess)
227+
default:
228+
break
229+
}
230+
enclosingScope = backlinks[parent]
231+
}
232+
}
233+
}
234+
235+
private extension Instruction {
236+
func mayHaveAccessScopeWhichConflicts(with beginAccess: BeginAccessInst, _ context: FunctionPassContext) -> Bool {
237+
if beginAccess.accessKind == .read {
238+
return mayWrite(toAddress: beginAccess.address, context.aliasAnalysis)
239+
} else {
240+
return mayReadOrWrite(address: beginAccess.address, context.aliasAnalysis)
241+
}
242+
}
243+
}
244+
245+
private extension BeginAccessInst {
246+
var isDead: Bool { users.allSatisfy({ $0.isIncidentalUse }) }
247+
}

SwiftCompilerSources/Sources/Optimizer/PassManager/PassRegistration.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ private func registerSwiftPasses() {
9494
registerPass(cleanupDebugStepsPass, { cleanupDebugStepsPass.run($0) })
9595
registerPass(namedReturnValueOptimization, { namedReturnValueOptimization.run($0) })
9696
registerPass(stripObjectHeadersPass, { stripObjectHeadersPass.run($0) })
97+
registerPass(deadAccessScopeElimination, { deadAccessScopeElimination.run($0) })
9798
registerPass(deadStoreElimination, { deadStoreElimination.run($0) })
9899
registerPass(redundantLoadElimination, { redundantLoadElimination.run($0) })
99100
registerPass(mandatoryRedundantLoadElimination, { mandatoryRedundantLoadElimination.run($0) })

SwiftCompilerSources/Sources/SIL/DataStructures/Set.swift

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,3 +287,66 @@ extension IntrusiveSet {
287287
insert(contentsOf: source)
288288
}
289289
}
290+
291+
/// A set which supports iterating over its elements.
292+
/// The iteration order is deterministic. All set operations are O(1).
293+
public struct IterableSet<Set: IntrusiveSet> : CollectionLikeSequence {
294+
public typealias Element = Set.Element
295+
296+
private var set: Set
297+
298+
// Erased elements do not get removed from `list`, because this would be O(n).
299+
private var list: Stack<Element>
300+
301+
// Elements which are in `list. This is different from `set` if elements get erased.
302+
private var inList: Set
303+
304+
public init(_ context: some Context) {
305+
self.set = Set(context)
306+
self.list = Stack(context)
307+
self.inList = Set(context)
308+
}
309+
310+
public mutating func deinitialize() {
311+
inList.deinitialize()
312+
list.deinitialize()
313+
set.deinitialize()
314+
}
315+
316+
public func contains(_ element: Element) -> Bool {
317+
set.contains(element)
318+
}
319+
320+
/// Returns true if `element` was not contained in the set before inserting.
321+
@discardableResult
322+
public mutating func insert(_ element: Element) -> Bool {
323+
if inList.insert(element) {
324+
list.append(element)
325+
}
326+
return set.insert(element)
327+
}
328+
329+
public mutating func erase(_ element: Element) {
330+
set.erase(element)
331+
}
332+
333+
public func makeIterator() -> Iterator { Iterator(base: list.makeIterator(), set: set) }
334+
335+
public struct Iterator: IteratorProtocol {
336+
var base: Stack<Element>.Iterator
337+
let set: Set
338+
339+
public mutating func next() -> Element? {
340+
while let n = base.next() {
341+
if set.contains(n) {
342+
return n
343+
}
344+
}
345+
return nil
346+
}
347+
}
348+
}
349+
350+
public typealias IterableInstructionSet = IterableSet<InstructionSet>
351+
public typealias SpecificIterableInstructionSet<InstType: Instruction> = IterableSet<SpecificInstructionSet<InstType>>
352+
public typealias IterableBasicBlockSet = IterableSet<BasicBlockSet>

SwiftCompilerSources/Sources/SIL/DataStructures/Worklist.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,54 @@ public struct Worklist<Set: IntrusiveSet> : CustomStringConvertible, NoReflectio
7070
}
7171
}
7272

73+
/// Like `Worklist` but can store an additional arbitrary payload per element.
74+
public struct WorklistWithPayload<Set: IntrusiveSet, Payload> : CustomStringConvertible, NoReflectionChildren {
75+
public typealias Element = Set.Element
76+
private var worklist: Stack<(Element, Payload)>
77+
private var pushedElements: Set
78+
79+
public init(_ context: some Context) {
80+
self.worklist = Stack(context)
81+
self.pushedElements = Set(context)
82+
}
83+
84+
public mutating func pop() -> (Element, Payload)? { return worklist.pop() }
85+
86+
public mutating func pushIfNotVisited(_ element: Element, with payload: Payload) {
87+
if pushedElements.insert(element) {
88+
worklist.append((element, payload))
89+
}
90+
}
91+
92+
public mutating func pushIfNotVisited<S: Sequence>(contentsOf other: S, with payload: Payload)
93+
where S.Element == Element
94+
{
95+
for element in other {
96+
pushIfNotVisited(element, with: payload)
97+
}
98+
}
99+
100+
/// Returns true if \p element was pushed to the worklist, regardless if it's already popped or not.
101+
public func hasBeenPushed(_ element: Element) -> Bool { pushedElements.contains(element) }
102+
103+
public var isEmpty: Bool { worklist.isEmpty }
104+
105+
public var description: String {
106+
"""
107+
worklist: \(worklist)
108+
pushed: \(pushedElements)
109+
"""
110+
}
111+
112+
/// TODO: once we have move-only types, make this a real deinit.
113+
public mutating func deinitialize() {
114+
pushedElements.deinitialize()
115+
worklist.deinitialize()
116+
}
117+
}
118+
73119
public typealias BasicBlockWorklist = Worklist<BasicBlockSet>
120+
public typealias BasicBlockWorklistWithPayload<Payload> = WorklistWithPayload<BasicBlockSet, Payload>
74121
public typealias InstructionWorklist = Worklist<InstructionSet>
75122
public typealias SpecificInstructionWorklist<InstType: Instruction> = Worklist<SpecificInstructionSet<InstType>>
76123
public typealias ValueWorklist = Worklist<ValueSet>

0 commit comments

Comments
 (0)