Skip to content

Commit 1dde9d4

Browse files
authored
Merge pull request #84071 from eeckstein/capture-propagation
Optimizer: rewrite and improve the ConstantCapturePropagation pass
2 parents 9d1436c + bb3b0b2 commit 1dde9d4

Some content is hidden

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

48 files changed

+1174
-908
lines changed

SwiftCompilerSources/Sources/AST/GenericSignature.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,6 @@ public struct GenericSignature: CustomStringConvertible, NoReflectionChildren {
3333
public func mapTypeIntoContext(_ type: Type) -> Type {
3434
Type(bridged: bridged.mapTypeIntoContext(type.bridged))
3535
}
36+
37+
public var isEmpty: Bool { bridged.impl == nil }
3638
}

SwiftCompilerSources/Sources/Optimizer/FunctionPasses/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ swift_compiler_sources(Optimizer
1111
AssumeSingleThreaded.swift
1212
AsyncDemotion.swift
1313
BooleanLiteralFolding.swift
14+
ConstantCapturePropagation.swift
1415
CleanupDebugSteps.swift
1516
ClosureSpecialization.swift
1617
ComputeEscapeEffects.swift
Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
//===--- ConstantCapturePropagation.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+
import AST
15+
16+
/// Propagates constant closure captures by specializing the partially applied function:
17+
///
18+
/// ```
19+
/// %1 = function_ref @closure
20+
/// %2 = integer_literal $Builtin.Int64, 27
21+
/// %3 = partial_apply %1(%2)
22+
///
23+
/// sil @closure : $(Builtin.Int64) -> () {
24+
/// bb0(%0 : $Builtin.Int64):
25+
/// ...
26+
/// ```
27+
/// ->
28+
/// ```
29+
/// %1 = function_ref @specialized_closure
30+
/// %3 = partial_apply %1()
31+
///
32+
/// sil @specialized_closure : $() -> () {
33+
/// bb0:
34+
/// %0 = integer_literal $Builtin.Int64, 27
35+
/// ...
36+
/// ```
37+
///
38+
/// Also, optimizes away a `partial_apply` of a thunk to a closure where all partially applied
39+
/// arguments are dead:
40+
///
41+
/// ```
42+
/// %2 = function_ref @thunk
43+
/// %3 = partial_apply %2(%1)
44+
///
45+
/// sil @thunk : $(T, V) -> () {
46+
/// bb0(%0 : $T, %1 : $V): // %1 is dead
47+
/// %2 = function_ref @closure
48+
/// %3 = apply %2(%0) // alive arguments are forwarded 1:1
49+
/// return %3
50+
/// }
51+
/// ```
52+
/// ->
53+
/// ```
54+
/// %2 = function_ref @closure
55+
/// %3 = thin_to_thick_function %2
56+
/// ```
57+
///
58+
let constantCapturePropagation = FunctionPass(name: "constant-capture-propagation") {
59+
(function: Function, context: FunctionPassContext) in
60+
61+
for inst in function.instructions {
62+
guard let partialApply = inst as? PartialApplyInst,
63+
// Only support closures which - after generic specialization - are not generic anymore.
64+
!partialApply.substitutionMap.replacementTypes.contains(where: { $0.hasArchetype })
65+
else {
66+
continue
67+
}
68+
69+
optimizeClosureWithDeadCaptures(of: partialApply, context)
70+
71+
if partialApply.isDeleted {
72+
continue
73+
}
74+
75+
constantPropagateCaptures(of: partialApply, context)
76+
}
77+
}
78+
79+
private func optimizeClosureWithDeadCaptures(of partialApply: PartialApplyInst, _ context: FunctionPassContext) {
80+
if let callee = getSpecializedCalleeWithDeadParams(of: partialApply, context) {
81+
rewritePartialApply(partialApply, withSpecialized: callee, arguments: [], context)
82+
}
83+
}
84+
85+
private func constantPropagateCaptures(of partialApply: PartialApplyInst, _ context: FunctionPassContext) {
86+
guard let callee = partialApply.referencedFunction,
87+
callee.isDefinition,
88+
let (constArgs, nonConstArgs) = partialApply.classifyArgumentsForConstness()
89+
else {
90+
return
91+
}
92+
93+
let specializedName = context.mangle(withConstantCaptureArguments: constArgs.map {
94+
(partialApply.calleeArgumentIndex(of: $0)!, $0.value)
95+
},
96+
from: callee)
97+
98+
let specializedCallee: Function
99+
if let existing = context.lookupFunction(name: specializedName) {
100+
specializedCallee = existing
101+
} else {
102+
specializedCallee = specializeClosure(specializedName: specializedName,
103+
partialApply: partialApply,
104+
constantArguments: constArgs, nonConstantArguments: nonConstArgs,
105+
context)
106+
}
107+
if !partialApply.isOnStack {
108+
// Escaping closures consume their arguments. Therefore we need to destroy the removed argument values.
109+
addCompensatingDestroys(for: constArgs, context)
110+
}
111+
let newArguments = nonConstArgs.map { $0.value }
112+
rewritePartialApply(partialApply, withSpecialized: specializedCallee, arguments: newArguments, context)
113+
}
114+
115+
private func getSpecializedCalleeWithDeadParams(of partialApply: PartialApplyInst,
116+
_ context: FunctionPassContext
117+
) -> Function? {
118+
guard let specialized = partialApply.getCalleeOfForwardingThunkWithDeadCaptures(),
119+
specialized.abi == .Swift
120+
else {
121+
return nil
122+
}
123+
124+
// Specialize the callee if it is generic
125+
if partialApply.substitutionMap.hasAnySubstitutableParams {
126+
guard specialized.isDefinition,
127+
partialApply.referencedFunction!.shouldOptimize, specialized.shouldOptimize
128+
else {
129+
return nil
130+
}
131+
132+
let genericSpecialized = context.specialize(function: specialized, for: partialApply.substitutionMap,
133+
convertIndirectToDirect: false, isMandatory: false)
134+
return genericSpecialized
135+
}
136+
137+
return specialized
138+
}
139+
140+
private func specializeClosure(specializedName: String,
141+
partialApply: PartialApplyInst,
142+
constantArguments: [Operand], nonConstantArguments: [Operand],
143+
_ context: FunctionPassContext
144+
) -> Function {
145+
let callee = partialApply.referencedFunction!
146+
var newParams = [ParameterInfo]()
147+
newParams.append(contentsOf: callee.convention.parameters.dropLast(partialApply.numArguments))
148+
newParams.append(contentsOf: nonConstantArguments.map { partialApply.parameter(for: $0)! })
149+
150+
let isGeneric = newParams.contains { $0.type.hasTypeParameter } ||
151+
callee.convention.results.contains { $0.type.hasTypeParameter() } ||
152+
callee.convention.errorResult?.type.hasTypeParameter() ?? false
153+
154+
let specializedClosure = context.createSpecializedFunctionDeclaration(from: callee,
155+
withName: specializedName,
156+
withParams: newParams,
157+
preserveGenericSignature: isGeneric)
158+
159+
context.buildSpecializedFunction(specializedFunction: specializedClosure) { (specializedClosure, specContext) in
160+
cloneAndSpecializeFunction(from: callee, toEmpty: specializedClosure,
161+
substitutions: partialApply.substitutionMap,
162+
specContext)
163+
164+
let entryBlock = specializedClosure.entryBlock
165+
for constArgOp in constantArguments {
166+
cloneArgument(constArgOp, of: partialApply, to: specializedClosure, specContext)
167+
}
168+
// Erase the cloned arguments from the entry block.
169+
for constArgOp in constantArguments.reversed() {
170+
let calleeArgIdx = partialApply.calleeArgumentIndex(of: constArgOp)!
171+
entryBlock.eraseArgument(at: calleeArgIdx, specContext)
172+
}
173+
}
174+
context.notifyNewFunction(function: specializedClosure, derivedFrom: callee)
175+
return specializedClosure
176+
}
177+
178+
private func cloneArgument(_ argumentOp: Operand,
179+
of partialApply: PartialApplyInst,
180+
to targetFunction: Function,
181+
_ context: FunctionPassContext
182+
) {
183+
var argCloner = Cloner(cloneBefore: targetFunction.entryBlock.instructions.first!, context)
184+
defer { argCloner.deinitialize() }
185+
186+
let clonedArg = argCloner.cloneRecursively(value: argumentOp.value)
187+
let calleeArgIdx = partialApply.calleeArgumentIndex(of: argumentOp)!
188+
let calleeArg = targetFunction.arguments[calleeArgIdx]
189+
calleeArg.uses.replaceAll(with: clonedArg, context)
190+
191+
if partialApply.calleeArgumentConventions[calleeArgIdx].isGuaranteed {
192+
// If the original argument was passed as guaranteed, i.e. is _not_ destroyed in the closure, we have
193+
// to destroy the cloned argument at function exits.
194+
for block in targetFunction.blocks where block.terminator.isFunctionExiting {
195+
let builder = Builder(before: block.terminator, context)
196+
builder.emitDestroy(of: clonedArg)
197+
}
198+
}
199+
200+
}
201+
202+
private func addCompensatingDestroys(for constantArguments: [Operand], _ context: FunctionPassContext) {
203+
for argOp in constantArguments {
204+
let builder = Builder(before: argOp.instruction, context)
205+
builder.emitDestroy(of: argOp.value)
206+
}
207+
}
208+
209+
private func rewritePartialApply(_ partialApply: PartialApplyInst, withSpecialized specialized: Function,
210+
arguments: [Value], _ context: FunctionPassContext) {
211+
let builder = Builder(before: partialApply, context)
212+
let fri = builder.createFunctionRef(specialized)
213+
let newClosure: Value
214+
if arguments.isEmpty {
215+
newClosure = builder.createThinToThickFunction(thinFunction: fri, resultType: partialApply.type)
216+
context.erase(instructions: partialApply.uses.users(ofType: DeallocStackInst.self))
217+
} else {
218+
newClosure = builder.createPartialApply(
219+
function: fri,
220+
substitutionMap: specialized.genericSignature.isEmpty ? SubstitutionMap() : partialApply.substitutionMap,
221+
capturedArguments: arguments, calleeConvention: partialApply.calleeConvention,
222+
hasUnknownResultIsolation: partialApply.hasUnknownResultIsolation, isOnStack: partialApply.isOnStack)
223+
}
224+
partialApply.uses.replaceAll(with: newClosure, context)
225+
226+
// Bypass any mark_dependence on the captures we specialized away.
227+
//
228+
// TODO: If we start to specialize away key path literals with operands (subscripts etc.), then a
229+
// dependence of the new partial_apply on those operands may still exist. However, we should still
230+
// leave the key path itself out of the dependency chain, and introduce dependencies on those
231+
// operands instead, so that the key path object itself can be made dead.
232+
for md in newClosure.uses.users(ofType: MarkDependenceInst.self) {
233+
if md.base.uses.getSingleUser(ofType: PartialApplyInst.self) == partialApply {
234+
md.replace(with: newClosure, context)
235+
}
236+
}
237+
context.erase(instruction: partialApply)
238+
}
239+
240+
private extension PartialApplyInst {
241+
242+
/// Returns the callee if this is a `partial_apply` of a thunk which directly forwards all arguments
243+
/// to the callee and has no other side-effects.
244+
func getCalleeOfForwardingThunkWithDeadCaptures() -> Function? {
245+
guard let thunk = referencedFunction,
246+
let thunkEntryBlock = thunk.blocks.first
247+
else {
248+
return nil
249+
}
250+
let numDeadArguments = self.arguments.count
251+
let numAliveArgs = thunkEntryBlock.arguments.count - numDeadArguments
252+
let deadCalleeArgs = thunkEntryBlock.arguments.dropFirst(numAliveArgs)
253+
254+
// TODO: handle non-trivial dead arguments, i.e. accept destroy instructions of such arguments.
255+
guard deadCalleeArgs.allSatisfy({ $0.type.isTrivial(in: thunk )}) else {
256+
return nil
257+
}
258+
259+
var callee: Function? = nil
260+
var returnValue: Value? = nil
261+
var errorValue: Value? = nil
262+
263+
for inst in thunk.instructions {
264+
switch inst {
265+
case let apply as ApplyInst:
266+
guard callee == nil, let c = apply.getCalleeWithForwardedArguments(numArguments: numAliveArgs) else {
267+
return nil
268+
}
269+
callee = c
270+
returnValue = apply
271+
case let tryApply as TryApplyInst:
272+
guard callee == nil,
273+
let c = tryApply.getCalleeWithForwardedArguments(numArguments: numAliveArgs)
274+
else {
275+
return nil
276+
}
277+
callee = c
278+
returnValue = tryApply.normalBlock.arguments.first
279+
errorValue = tryApply.errorBlock.arguments.first
280+
case let returnInst as ReturnInst:
281+
guard let returnValue, returnInst.returnedValue == returnValue else {
282+
return nil
283+
}
284+
case let throwInst as ThrowInst:
285+
guard let errorValue, throwInst.thrownValue == errorValue else {
286+
return nil
287+
}
288+
case is TermInst:
289+
return nil
290+
default:
291+
if inst.mayHaveSideEffects {
292+
return nil
293+
}
294+
}
295+
}
296+
return callee
297+
}
298+
299+
func classifyArgumentsForConstness() -> (constArguments: [Operand], nonConstArguments: [Operand])? {
300+
var constArgs = [Operand]()
301+
var nonConstArgs = [Operand]()
302+
var hasKeypath = false
303+
for argOp in argumentOperands {
304+
if argOp.value.isConstant(hasKeypath: &hasKeypath) {
305+
constArgs.append(argOp)
306+
} else {
307+
nonConstArgs.append(argOp)
308+
}
309+
}
310+
311+
// The optimization is beneficial if we can either get rid of all captures, because this
312+
// avoids allocating the context.
313+
// Or if we can constant propagate at least one keypath. Keypaths are so expensive that constant
314+
// propagating a single keypath is already beneficial.
315+
if !constArgs.isEmpty,
316+
nonConstArgs.isEmpty || hasKeypath
317+
{
318+
return (constArgs, nonConstArgs)
319+
}
320+
return nil
321+
}
322+
}
323+
324+
private extension FullApplySite {
325+
func getCalleeWithForwardedArguments(numArguments: Int) -> Function? {
326+
if let callee = referencedFunction,
327+
callee.numArguments == numArguments,
328+
zip(parentFunction.entryBlock.arguments, arguments).allSatisfy({ $0.0 == $0.1 })
329+
{
330+
return callee
331+
}
332+
return nil
333+
}
334+
}
335+
336+
private extension Value {
337+
func isConstant(hasKeypath: inout Bool) -> Bool {
338+
// All instructions handled here must also be handled in
339+
// `FunctionSignatureSpecializationMangler::mangleConstantProp`.
340+
switch self {
341+
case let si as StructInst:
342+
for op in si.operands {
343+
if !op.value.isConstant(hasKeypath: &hasKeypath) {
344+
return false
345+
}
346+
}
347+
return true
348+
case is ThinToThickFunctionInst, is ConvertFunctionInst, is UpcastInst, is OpenExistentialRefInst:
349+
return (self as! UnaryInstruction).operand.value.isConstant(hasKeypath: &hasKeypath)
350+
case is StringLiteralInst, is IntegerLiteralInst, is FloatLiteralInst, is FunctionRefInst, is GlobalAddrInst:
351+
return true
352+
case let keyPath as KeyPathInst:
353+
hasKeypath = true
354+
guard keyPath.operands.isEmpty,
355+
keyPath.hasPattern,
356+
!keyPath.substitutionMap.hasAnySubstitutableParams
357+
else {
358+
return false
359+
}
360+
return true
361+
default:
362+
return false
363+
}
364+
}
365+
}

0 commit comments

Comments
 (0)