Files
swift-mirror/SwiftCompilerSources/Sources/Optimizer/Utilities/Devirtualization.swift
Erik Eckstein 6dde4c942e MandatoryPerformanceOptimizations: force de-virtualizing value-type deinits
Do this even if the function then contains references to other functions with wrong linkage.
MandatoryPerformanceOptimization fixes the linkage afterwards.
This is similar to what we already do with de-virtualizing class and witness methods: https://github.com/swiftlang/swift/pull/76032

rdar://168171608
2026-02-02 19:36:48 +01:00

351 lines
14 KiB
Swift

//===--- Devirtualization.swift -------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
import SIL
/// Devirtualizes all value-type deinitializers of a `destroy_value`.
///
/// This may be a no-op if the destroy doesn't call any deinitializers.
/// Returns true if all deinitializers could be devirtualized.
func devirtualizeDeinits(of destroy: DestroyValueInst, isMandatory: Bool, _ context: some MutatingContext) -> Bool {
return devirtualize(destroy: destroy, isMandatory: isMandatory, context)
}
/// Devirtualizes all value-type deinitializers of a `destroy_addr`.
///
/// This may be a no-op if the destroy doesn't call any deinitializers.
/// Returns true if all deinitializers could be devirtualized.
func devirtualizeDeinits(of destroy: DestroyAddrInst, isMandatory: Bool, _ context: some MutatingContext) -> Bool {
return devirtualize(destroy: destroy, isMandatory: isMandatory, context)
}
func devirtualizeDeinits(of builtin: BuiltinInst, isMandatory: Bool, _ context: some MutatingContext) -> Bool {
switch builtin.id {
case .DestroyArray:
return devirtualize(builtinDestroyArray: builtin, isMandatory: isMandatory, context)
default:
return true
}
}
private func devirtualize(destroy: some DevirtualizableDestroy,
isMandatory: Bool,
_ context: some MutatingContext
) -> Bool {
let type = destroy.type
if !type.isMoveOnly {
return true
}
guard let nominal = type.nominal else {
// E.g. a non-copyable generic function parameter
return true
}
// We cannot de-virtualize C++ destructor calls of C++ move-only types because we cannot get
// its destructor (`nominal.valueTypeDestructor` is nil).
if nominal.hasClangNode {
return false
}
if nominal.valueTypeDestructor != nil && !destroy.shouldDropDeinit {
guard let deinitFunc = context.lookupDeinit(ofNominal: nominal) else {
return false
}
// In mandatory mode, de-virtualization might create function references to functions with wrong linkage.
// MandatoryPerformanceOptimization fixes this later.
if !isMandatory {
// In non-mandatory mode, we don't allow this.
let serialized = destroy.parentFunction.serializedKind
guard serialized == .notSerialized || deinitFunc.hasValidLinkageForFragileRef(serialized) else {
return false
}
}
if deinitFunc.linkage == .shared && !deinitFunc.isDefinition {
// Make sure to not have an external shared function, which is illegal in SIL.
_ = context.loadFunction(function: deinitFunc, loadCalleesRecursively: false)
}
destroy.createDeinitCall(to: deinitFunc, context)
context.erase(instruction: destroy)
return true
}
// If there is no deinit to be called for the original type we have to recursively visit
// the struct fields or enum cases.
if type.isStruct {
return destroy.devirtualizeStructFields(isMandatory: isMandatory, context)
}
if type.isEnum {
return destroy.devirtualizeEnumPayloads(isMandatory: isMandatory, context)
}
precondition(type.isClass, "unknown non-copyable type")
// A class reference cannot be further de-composed.
return true
}
// Used to dispatch devirtualization tasks to `destroy_value` and `destroy_addr`.
private protocol DevirtualizableDestroy : UnaryInstruction {
var shouldDropDeinit: Bool { get }
func createDeinitCall(to deinitializer: Function, _ context: some MutatingContext)
func devirtualizeStructFields(isMandatory: Bool, _ context: some MutatingContext) -> Bool
func devirtualizeEnumPayload(enumCase: EnumCase,
in block: BasicBlock,
isMandatory: Bool,
_ context: some MutatingContext) -> Bool
func createSwitchEnum(atEndOf block: BasicBlock, cases: [(Int, BasicBlock)], _ context: some MutatingContext)
}
private extension DevirtualizableDestroy {
var type: Type { operand.value.type }
func devirtualizeEnumPayloads(isMandatory: Bool, _ context: some MutatingContext) -> Bool {
guard let cases = type.getEnumCases(in: parentFunction) else {
return false
}
defer {
context.erase(instruction: self)
}
if cases.allPayloadsAreTrivial(in: parentFunction) {
let builder = Builder(before: self, context)
builder.createEndLifetime(of: operand.value)
return true
}
var caseBlocks: [(caseIndex: Int, targetBlock: BasicBlock)] = []
let switchBlock = parentBlock
let endBlock = context.splitBlock(before: self)
var result = true
for enumCase in cases {
let caseBlock = context.createBlock(after: switchBlock)
caseBlocks.append((enumCase.index, caseBlock))
let builder = Builder(atEndOf: caseBlock, location: location, context)
builder.createBranch(to: endBlock)
if !devirtualizeEnumPayload(enumCase: enumCase, in: caseBlock, isMandatory: isMandatory, context) {
result = false
}
}
createSwitchEnum(atEndOf: switchBlock, cases: caseBlocks, context)
return result
}
}
extension DestroyValueInst : DevirtualizableDestroy {
fileprivate var shouldDropDeinit: Bool { operand.value.lookThoughOwnershipInstructions is DropDeinitInst }
fileprivate func createDeinitCall(to deinitializer: Function, _ context: some MutatingContext) {
let builder = Builder(before: self, context)
let subs = context.getContextSubstitutionMap(for: type)
let deinitRef = builder.createFunctionRef(deinitializer)
if deinitializer.argumentConventions[deinitializer.selfArgumentIndex!].isIndirect {
let allocStack = builder.createAllocStack(type)
builder.createStore(source: destroyedValue, destination: allocStack, ownership: .initialize)
builder.createApply(function: deinitRef, subs, arguments: [allocStack])
builder.createDeallocStack(allocStack)
} else {
builder.createApply(function: deinitRef, subs, arguments: [destroyedValue])
}
}
fileprivate func devirtualizeStructFields(isMandatory: Bool, _ context: some MutatingContext) -> Bool {
guard let fields = type.getNominalFields(in: parentFunction) else {
return false
}
defer {
context.erase(instruction: self)
}
let builder = Builder(before: self, context)
if fields.allFieldsAreTrivial(in: parentFunction) {
builder.createEndLifetime(of: operand.value)
return true
}
let destructure = builder.createDestructureStruct(struct: destroyedValue)
var result = true
for fieldValue in destructure.results where !fieldValue.type.isTrivial(in: parentFunction) {
let destroyField = builder.createDestroyValue(operand: fieldValue)
if !devirtualizeDeinits(of: destroyField, isMandatory: isMandatory, context) {
result = false
}
}
return result
}
fileprivate func devirtualizeEnumPayload(
enumCase: EnumCase,
in block: BasicBlock,
isMandatory: Bool,
_ context: some MutatingContext
) -> Bool {
let builder = Builder(atBeginOf: block, location: location, context)
if let payloadTy = enumCase.payload {
let payload = block.addArgument(type: payloadTy, ownership: .owned, context)
if !payloadTy.isTrivial(in: parentFunction) {
let destroyPayload = builder.createDestroyValue(operand: payload)
return devirtualizeDeinits(of: destroyPayload, isMandatory: isMandatory, context)
}
}
return true
}
fileprivate func createSwitchEnum(
atEndOf block: BasicBlock,
cases: [(Int, BasicBlock)],
_ context: some MutatingContext
) {
let builder = Builder(atEndOf: block, location: location, context)
builder.createSwitchEnum(enum: destroyedValue, cases: cases)
}
}
extension DestroyAddrInst : DevirtualizableDestroy {
fileprivate var shouldDropDeinit: Bool {
// The deinit is always called by a destroy_addr. There must not be a `drop_deinit` as operand.
false
}
fileprivate func createDeinitCall(to deinitializer: Function, _ context: some MutatingContext) {
let builder = Builder(before: self, context)
let subs = context.getContextSubstitutionMap(for: destroyedAddress.type)
let deinitRef = builder.createFunctionRef(deinitializer)
if !deinitializer.argumentConventions[deinitializer.selfArgumentIndex!].isIndirect {
let value = builder.createLoad(fromAddress: destroyedAddress, ownership: .take)
builder.createApply(function: deinitRef, subs, arguments: [value])
} else {
builder.createApply(function: deinitRef, subs, arguments: [destroyedAddress])
}
}
fileprivate func devirtualizeStructFields(isMandatory: Bool, _ context: some MutatingContext) -> Bool {
let builder = Builder(before: self, context)
guard let fields = type.getNominalFields(in: parentFunction) else {
return false
}
defer {
context.erase(instruction: self)
}
if fields.allFieldsAreTrivial(in: parentFunction) {
builder.createEndLifetime(of: operand.value)
return true
}
var result = true
for (fieldIdx, fieldTy) in fields.enumerated()
where !fieldTy.isTrivial(in: parentFunction)
{
let fieldAddr = builder.createStructElementAddr(structAddress: destroyedAddress, fieldIndex: fieldIdx)
let destroyField = builder.createDestroyAddr(address: fieldAddr)
if !devirtualizeDeinits(of: destroyField, isMandatory: isMandatory, context) {
result = false
}
}
return result
}
fileprivate func devirtualizeEnumPayload(
enumCase: EnumCase,
in block: BasicBlock,
isMandatory: Bool,
_ context: some MutatingContext
) -> Bool {
let builder = Builder(atBeginOf: block, location: location, context)
if let payloadTy = enumCase.payload,
!payloadTy.isTrivial(in: parentFunction)
{
let caseAddr = builder.createUncheckedTakeEnumDataAddr(enumAddress: destroyedAddress, caseIndex: enumCase.index)
let destroyPayload = builder.createDestroyAddr(address: caseAddr)
return devirtualizeDeinits(of: destroyPayload, isMandatory: isMandatory, context)
}
return true
}
fileprivate func createSwitchEnum(
atEndOf block: BasicBlock,
cases: [(Int, BasicBlock)],
_ context: some MutatingContext
) {
let builder = Builder(atEndOf: block, location: location, context)
builder.createSwitchEnumAddr(enumAddress: destroyedAddress, cases: cases)
}
}
private func devirtualize(builtinDestroyArray: BuiltinInst,
isMandatory: Bool,
_ context: some MutatingContext
) -> Bool {
let function = builtinDestroyArray.parentFunction
let elementType = builtinDestroyArray.substitutionMap.replacementType.loweredType(in: function)
guard elementType.isMoveOnly,
// This avoids lowering the loop if the element is a non-copyable generic type.
(elementType.isStruct || elementType.isEnum)
else {
return true
}
// Lower the `builtin "destroyArray" to a loop which destroys all elements
let basePointer = builtinDestroyArray.arguments[1]
let arrayCount = builtinDestroyArray.arguments[2]
let indexType = arrayCount.type
let boolType = context.getBuiltinIntegerType(bitWidth: 1)
let preheaderBlock = builtinDestroyArray.parentBlock
let exitBlock = context.splitBlock(after: builtinDestroyArray)
let headerBlock = context.createBlock(after: preheaderBlock)
let bodyBlock = context.createBlock(after: headerBlock)
let preheaderBuilder = Builder(atEndOf: preheaderBlock, location: builtinDestroyArray.location, context)
let zero = preheaderBuilder.createIntegerLiteral(0, type: indexType)
let one = preheaderBuilder.createIntegerLiteral(1, type: indexType)
let falseValue = preheaderBuilder.createBoolLiteral(false);
let baseAddress = preheaderBuilder.createPointerToAddress(pointer: basePointer,
addressType: elementType.addressType,
isStrict: true, isInvariant: false)
preheaderBuilder.createBranch(to: headerBlock, arguments: [zero])
let inductionVariable = headerBlock.addArgument(type: indexType, ownership: .none, context)
let headerBuilder = Builder(atEndOf: headerBlock, location: builtinDestroyArray.location, context)
let cmp = headerBuilder.createBuiltinBinaryFunction(name: "cmp_slt", operandType: indexType, resultType: boolType,
arguments: [inductionVariable, arrayCount])
headerBuilder.createCondBranch(condition: cmp, trueBlock: bodyBlock, falseBlock: exitBlock)
let bodyBuilder = Builder(atEndOf: bodyBlock, location: builtinDestroyArray.location, context)
let elementAddr = bodyBuilder.createIndexAddr(base: baseAddress, index: inductionVariable,
needStackProtection: false)
let destroy = bodyBuilder.createDestroyAddr(address: elementAddr)
let resultType = context.getTupleType(elements: [indexType, boolType]).loweredType(in: function)
let increment = bodyBuilder.createBuiltinBinaryFunction(name: "sadd_with_overflow", operandType: indexType,
resultType: resultType,
arguments: [inductionVariable, one, falseValue])
let incrResult = bodyBuilder.createTupleExtract(tuple: increment, elementIndex: 0)
bodyBuilder.createBranch(to: headerBlock, arguments: [incrResult])
context.erase(instruction: builtinDestroyArray)
return devirtualize(destroy: destroy, isMandatory: isMandatory, context)
}
private extension EnumCases {
func allPayloadsAreTrivial(in function: Function) -> Bool {
allSatisfy({ $0.payload?.isTrivial(in: function) ?? true })
}
}
private extension NominalFieldsArray {
func allFieldsAreTrivial(in function: Function) -> Bool {
allSatisfy({ $0.isTrivial(in: function)})
}
}