Skip to content

Commit b3d9b87

Browse files
authored
Merge pull request #58888 from eeckstein/objc-string-opt
Optimizer: add the ObjCBridgingOptimization to optimize ObjectiveC bridging operations.
2 parents e1412a0 + 9f70c29 commit b3d9b87

File tree

17 files changed

+1012
-598
lines changed

17 files changed

+1012
-598
lines changed

SwiftCompilerSources/Sources/Optimizer/FunctionPasses/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ swift_compiler_sources(Optimizer
1010
AssumeSingleThreaded.swift
1111
ComputeEffects.swift
1212
EscapeInfoDumper.swift
13+
ObjCBridgingOptimization.swift
1314
SILPrinter.swift
1415
MergeCondFails.swift
1516
RangeDumper.swift
Lines changed: 384 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,384 @@
1+
//===--- ObjCBridgingOptimization.swift - optimize ObjC bridging ----------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2022 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 Basic
14+
import SIL
15+
16+
/// Removes redundant ObjectiveC <-> Swift bridging calls.
17+
///
18+
/// Basically, if a value is bridged from ObjectiveC to Swift an then back to ObjectiveC
19+
/// again, then just re-use the original ObjectiveC value.
20+
///
21+
/// Things get a little bit more complicated in case of optionals (Nullable pointers).
22+
/// In this case both bridging calls are embedded in an `switch_enum` CFG diamond, like
23+
/// ```
24+
/// switch_enum %originalOptionalObjcValue
25+
/// some_bb(%1):
26+
/// %2 = enum #some(%1)
27+
/// %3 = apply %bridgeFromObjc(%2)
28+
/// %4 = enum #some(%3)
29+
/// br continue_bb(%4)
30+
/// none_bb:
31+
/// %5 = enum #none
32+
/// br continue_bb(%5)
33+
/// continue_bb(%bridgedOptionalSwiftValue):
34+
/// ```
35+
let objCBridgingOptimization = FunctionPass(name: "objc-bridging-opt", {
36+
(function: Function, context: PassContext) in
37+
38+
if !function.hasOwnership { return }
39+
40+
// First try to optimize the optional -> optional case.
41+
// We need to do this before handling the non-optional case to prevent
42+
// sub-optimal optimization of bridging calls inside a switch_enum.
43+
for block in function.blocks {
44+
// Start at a block argument, which is the "result" of the switch_enum CFG diamond.
45+
if !optimizeOptionalBridging(forArgumentOf: block, context) {
46+
return
47+
}
48+
}
49+
50+
// Now try to optimize non-optional and optional -> non-optional bridging.
51+
for block in function.blocks {
52+
for inst in block.instructions {
53+
if let apply = inst as? ApplyInst {
54+
if !optimizeNonOptionalBridging(apply, context) {
55+
return
56+
}
57+
}
58+
}
59+
}
60+
})
61+
62+
//===----------------------------------------------------------------------===//
63+
// Top-level optimization functions
64+
//===----------------------------------------------------------------------===//
65+
66+
/// Optimizes redundant bridging calls where both calls are within `switch_enum` diamonds.
67+
///
68+
/// For example:
69+
/// ```
70+
/// let s = returnOptionalNSString()
71+
/// useOptionalNSString(s)
72+
/// ```
73+
///
74+
/// The `block` is the continue-block of the second `switch_enum` diamond.
75+
/// Returns true if the pass should continue running.
76+
private func optimizeOptionalBridging(forArgumentOf block: BasicBlock,
77+
_ context: PassContext) -> Bool {
78+
if block.arguments.count != 1 {
79+
// For simplicity only handle the common case: there is only one phi-argument which
80+
// is the result of the bridging operation.
81+
return true
82+
}
83+
// Check for the second swift -> ObjC bridging operation.
84+
let finalObjCValue = block.arguments[0]
85+
guard let swiftValueSwitch = isOptionalBridging(of: finalObjCValue, isBridging: isBridgeToObjcCall) else {
86+
return true
87+
}
88+
89+
// Check for the first ObjC -> swift bridging operation.
90+
let swiftValue = lookThroughOwnershipInsts(swiftValueSwitch.enumOp)
91+
guard let originalObjCValueSwitch = isOptionalBridging(of: swiftValue, isBridging: isBridgeToSwiftCall) else {
92+
return true
93+
}
94+
95+
let originalObjCValue = originalObjCValueSwitch.enumOp
96+
if finalObjCValue.type != originalObjCValue.type {
97+
return true
98+
}
99+
100+
if !context.continueWithNextSubpassRun(for: originalObjCValueSwitch) {
101+
return false
102+
}
103+
104+
// The second bridging operation can be in a different control region than the first one,
105+
// e.g. it can be in a loop whereas the first is not in that loop. Therefore we have to
106+
// copy + makeAvailable.
107+
let replacement = originalObjCValue.copy(at: originalObjCValueSwitch,
108+
andMakeAvailableIn: block, context)
109+
110+
finalObjCValue.uses.replaceAll(with: replacement, context)
111+
block.eraseArgument(at: 0, context)
112+
113+
// The swift -> ObjC bridging call has no `readonly` attribute, therefore we have to
114+
// explicitly delete it. The ObjC -> swift call has such an attribute and will be removed
115+
// buy a later dead-code elimination pass.
116+
removeBridgingCodeInPredecessors(of: block, context)
117+
return true
118+
}
119+
120+
/// Optimizes redundant bridging calls where the second call is a non-optional bridging operation,
121+
/// i.e. is _not_ within `switch_enum` diamond.
122+
///
123+
/// The `apply` is the second (swift -> ObjC) bridging call.
124+
/// Returns true if the pass should continue running.
125+
private func optimizeNonOptionalBridging(_ apply: ApplyInst,
126+
_ context: PassContext) -> Bool {
127+
128+
guard let bridgeToObjcCall = isBridgeToObjcCall(apply) else {
129+
return true
130+
}
131+
132+
let swiftValue = lookThroughOwnershipInsts(bridgeToObjcCall.arguments[0])
133+
134+
// Handle the first case: the ObjC -> swift bridging operation is optional and the swift -> ObjC
135+
// bridging is within a test for Optional.some, e.g.
136+
// ```
137+
// if let s = returnOptionalNSString() {
138+
// useNonOptionalNSString(s)
139+
// }
140+
// ```
141+
if let (se, someCase) = isPayloadOfSwitchEnum(swiftValue),
142+
let originalObjCValueSwitch = isOptionalBridging(of: se.enumOp, isBridging: isBridgeToSwiftCall) {
143+
144+
if !context.continueWithNextSubpassRun(for: originalObjCValueSwitch) {
145+
return false
146+
}
147+
148+
let originalObjCValue = originalObjCValueSwitch.enumOp
149+
let optionalReplacement = originalObjCValue.copy(at: originalObjCValueSwitch,
150+
andMakeAvailableIn: bridgeToObjcCall.block, context)
151+
let builder = Builder(at: bridgeToObjcCall, context)
152+
153+
// We know that it's the some-case.
154+
let replacement = builder.createUncheckedEnumData(enum: optionalReplacement,
155+
caseIndex: someCase,
156+
resultType: bridgeToObjcCall.type)
157+
bridgeToObjcCall.uses.replaceAll(with: replacement, context)
158+
context.erase(instruction: bridgeToObjcCall)
159+
return true
160+
}
161+
162+
// Handle the second case: both bridging calls are non-optional, e.g.
163+
// ```
164+
// let s = returnNonOptionalNSString()
165+
// useNonOptionalNSString(s)
166+
// ```
167+
guard let bridgeToSwiftCall = isBridgeToSwiftCall(swiftValue) else {
168+
return true
169+
}
170+
171+
if !context.continueWithNextSubpassRun(for: bridgeToSwiftCall) {
172+
return false
173+
}
174+
175+
let originalObjCValue = bridgeToSwiftCall.arguments[0]
176+
let optionalObjCType = originalObjCValue.type
177+
178+
// The bridging functions from ObjC -> Swift take an optional argument and return a
179+
// non-optional Swift value. In the nil-case they return an empty (e.g. empty String,
180+
// empty Array, etc.) swift value.
181+
// We have to replicate that behavior here.
182+
183+
guard let someCase = optionalObjCType.getIndexOfEnumCase(withName: "some") else { return true }
184+
guard let noneCase = optionalObjCType.getIndexOfEnumCase(withName: "none") else { return true }
185+
186+
// Creates a `switch_enum` on `originalObjCValue` and in the nil-case return a bridged
187+
// empty value.
188+
// Create the needed blocks of the `switch_enum` CFG diamond.
189+
let origBlock = bridgeToSwiftCall.block
190+
let someBlock = context.splitBlock(at: bridgeToSwiftCall)
191+
let noneBlock = context.splitBlock(at: bridgeToSwiftCall)
192+
let continueBlock = context.splitBlock(at: bridgeToSwiftCall)
193+
194+
195+
let builder = Builder(atEndOf: origBlock, location: bridgeToSwiftCall.location, context)
196+
let copiedValue = builder.createCopyValue(operand: originalObjCValue)
197+
builder.createSwitchEnum(enum: copiedValue, cases: [(someCase, someBlock),
198+
(noneCase, noneBlock)])
199+
200+
// The nil case: call the ObjC -> Swift bridging function, which will return
201+
// an empty swift value.
202+
let noneBuilder = Builder(atEndOf: noneBlock, location: bridgeToSwiftCall.location, context)
203+
let subst = bridgeToObjcCall.substitutionMap
204+
let emptySwiftValue = noneBuilder.createApply(
205+
function: bridgeToSwiftCall.callee,
206+
bridgeToSwiftCall.substitutionMap, arguments: Array(bridgeToSwiftCall.arguments))
207+
// ... and bridge that to ObjectiveC.
208+
let emptyObjCValue = noneBuilder.createApply(
209+
function: noneBuilder.createFunctionRef(bridgeToObjcCall.referencedFunction!),
210+
subst, arguments: [emptySwiftValue])
211+
noneBuilder.createDestroyValue(operand: emptySwiftValue)
212+
noneBuilder.createBranch(to: continueBlock, arguments: [emptyObjCValue])
213+
214+
// In the some-case just forward the original NSString.
215+
let objCType = emptyObjCValue.type
216+
let forwardedValue = someBlock.addBlockArgument(type: objCType, ownership: .owned, context)
217+
let someBuilder = Builder(atEndOf: someBlock, location: bridgeToSwiftCall.location, context)
218+
someBuilder.createBranch(to: continueBlock, arguments: [forwardedValue])
219+
220+
let s = continueBlock.addBlockArgument(type: objCType, ownership: .owned, context)
221+
222+
// Now replace the bridged value with the original value in the destination block.
223+
let replacement = s.makeAvailable(in: bridgeToObjcCall.block, context)
224+
bridgeToObjcCall.uses.replaceAll(with: replacement, context)
225+
context.erase(instruction: bridgeToObjcCall)
226+
return true
227+
}
228+
229+
//===----------------------------------------------------------------------===//
230+
// Utility functions
231+
//===----------------------------------------------------------------------===//
232+
233+
/// Removes `enum` instructions and bridging calls in all predecessors of `block`.
234+
private func removeBridgingCodeInPredecessors(of block: BasicBlock, _ context: PassContext) {
235+
for pred in block.predecessors {
236+
let branch = pred.terminator as! BranchInst
237+
let builder = Builder(after: branch, context)
238+
builder.createBranch(to: block)
239+
240+
let en = branch.operands[0].value as! EnumInst
241+
context.erase(instruction: branch)
242+
let op = en.operand
243+
context.erase(instruction: en)
244+
if let bridgingCall = op {
245+
context.erase(instruction: bridgingCall as! ApplyInst)
246+
}
247+
}
248+
}
249+
250+
private func lookThroughOwnershipInsts(_ value: Value) -> Value {
251+
// Looks like it's sufficient to support begin_borrow for now.
252+
// TODO: add copy_value if needed.
253+
if let bbi = value as? BeginBorrowInst {
254+
return bbi.operand
255+
}
256+
return value
257+
}
258+
259+
/// Checks for an optional bridging `switch_enum` diamond.
260+
///
261+
/// ```
262+
/// switch_enum %0 // returned instruction
263+
/// some_bb(%1):
264+
/// %2 = enum #some(%1) // only in case of ObjC -> Swift briding
265+
/// %3 = apply %bridging(%2) // returned by `isBridging`
266+
/// %4 = enum #some(%3)
267+
/// br continue_bb(%4)
268+
/// none_bb:
269+
/// %5 = enum #none
270+
/// br continue_bb(%5)
271+
/// continue_bb(%value): // passed value
272+
/// ```
273+
private func isOptionalBridging(of value: Value, isBridging: (Value) -> ApplyInst?) -> SwitchEnumInst? {
274+
guard let arg = value as? BlockArgument,
275+
arg.isPhiArgument else {
276+
return nil
277+
}
278+
279+
var noneSwitch: SwitchEnumInst?
280+
var someSwitch: SwitchEnumInst?
281+
282+
// Check if one incoming value is the none-case and the other is the some-case.
283+
for incomingVal in arg.incomingPhiValues {
284+
// In both branches, the result must be an `enum` which is passed to the
285+
// continue_bb's phi-argument.
286+
guard let enumInst = incomingVal as? EnumInst,
287+
let singleEnumUse = enumInst.uses.singleUse,
288+
singleEnumUse.instruction is BranchInst else {
289+
return nil
290+
}
291+
if let enumOp = enumInst.operand {
292+
// The some-case
293+
if someSwitch != nil { return nil }
294+
guard let bridgingCall = isBridging(enumOp),
295+
bridgingCall.uses.isSingleUse else {
296+
return nil
297+
}
298+
let callArgument = bridgingCall.arguments[0]
299+
300+
// If it's an ObjC -> Swift bridging call the argument is wrapped into an optional enum.
301+
if callArgument.type.isEnum {
302+
guard let sourceEnum = callArgument as? EnumInst,
303+
let sourceEnumOp = sourceEnum.operand,
304+
let (se, someCase) = isPayloadOfSwitchEnum(sourceEnumOp),
305+
enumInst.caseIndex == someCase,
306+
sourceEnum.caseIndex == someCase,
307+
sourceEnum.type == se.enumOp.type else {
308+
return nil
309+
}
310+
someSwitch = se
311+
} else {
312+
guard let (se, someCase) = isPayloadOfSwitchEnum(callArgument),
313+
enumInst.caseIndex == someCase else {
314+
return nil
315+
}
316+
someSwitch = se
317+
}
318+
} else {
319+
// The none-case
320+
if noneSwitch != nil { return nil }
321+
guard let singlePred = enumInst.block.singlePredecessor,
322+
let se = singlePred.terminator as? SwitchEnumInst,
323+
se.getUniqueSuccessor(forCaseIndex: enumInst.caseIndex) === enumInst.block else {
324+
return nil
325+
}
326+
noneSwitch = se
327+
}
328+
}
329+
guard let noneSwitch = noneSwitch,
330+
let someSwitch = someSwitch,
331+
noneSwitch == someSwitch else {
332+
return nil
333+
}
334+
return someSwitch
335+
}
336+
337+
/// Returns the `switch_enum` together with the enum case index, if `value` is
338+
/// the payload block argument of the `switch_enum`.
339+
private func isPayloadOfSwitchEnum(_ value: Value) -> (SwitchEnumInst, case: Int)? {
340+
if let payloadArg = value as? BlockArgument,
341+
let pred = payloadArg.block.singlePredecessor,
342+
let se = pred.terminator as? SwitchEnumInst,
343+
let caseIdx = se.getUniqueCase(forSuccessor: payloadArg.block) {
344+
return (se, caseIdx)
345+
}
346+
return nil
347+
}
348+
349+
/// Returns the apply instruction if `value` is an ObjC -> Swift bridging call.
350+
func isBridgeToSwiftCall(_ value: Value) -> ApplyInst? {
351+
guard let bridgingCall = value as? ApplyInst,
352+
let bridgingFunc = bridgingCall.referencedFunction else {
353+
return nil
354+
}
355+
let funcName = bridgingFunc.name
356+
guard bridgingFunc.hasSemanticsAttribute("bridgeFromObjectiveC") ||
357+
// Currently the semantics attribute is not used, so test for specific functions, too.
358+
// TODO: remove those checks once the briding functions are annotate with "bridgeFromObjectiveC"
359+
// in Foundation.
360+
//
361+
// String._unconditionallyBridgeFromObjectiveC(_:)
362+
funcName == "$sSS10FoundationE36_unconditionallyBridgeFromObjectiveCySSSo8NSStringCSgFZ" ||
363+
// Array._unconditionallyBridgeFromObjectiveC(_:)
364+
funcName == "$sSa10FoundationE36_unconditionallyBridgeFromObjectiveCySayxGSo7NSArrayCSgFZ" else {
365+
return nil
366+
}
367+
guard bridgingCall.arguments.count == 2,
368+
bridgingCall.getArgumentConvention(calleeArgIndex: 0) == .directGuaranteed else {
369+
return nil
370+
}
371+
return bridgingCall
372+
}
373+
374+
/// Returns the apply instruction if `value` is a Swift -> ObjC bridging call.
375+
func isBridgeToObjcCall(_ value: Value) -> ApplyInst? {
376+
guard let bridgingCall = value as? ApplyInst,
377+
let bridgingFunc = bridgingCall.referencedFunction,
378+
bridgingFunc.hasSemanticsAttribute("convertToObjectiveC"),
379+
bridgingCall.arguments.count == 1,
380+
bridgingCall.getArgumentConvention(calleeArgIndex: 0) == .directGuaranteed else {
381+
return nil
382+
}
383+
return bridgingCall
384+
}

0 commit comments

Comments
 (0)