mirror of
https://github.com/apple/swift.git
synced 2026-06-20 15:42:51 +02:00
7ea1b1b23e
The `projection` flag indicates that `index_addr` projects an element address from an array base address, as opposed to being used for general pointer arithmetic. When this flag is set, the result address can only reach the single element at the given index — it is not possible to chain multiple `index_addr` instructions to reach other array elements from the result. Without this flag, the result may be used as the base of another `index_addr`, allowing arithmetic across element boundaries (e.g. an `index_addr` with index 1 followed by an `index_addr` with index 2 reaches the element at offset 3). An `index_addr [projection]` is mandatory to go from an array base address to an element - even if it's the first element, i.e. the index is zero. This means that the optimizer must not remove `index_addr [projection]` with a zero index.
419 lines
17 KiB
Swift
419 lines
17 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 AST
|
|
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 {
|
|
if destroy.isDeadEnd {
|
|
// It doesn't make sense to de-virtualize `destroy_value [dead_end]` because such operations
|
|
// are no-ops anyway. This is especially important for Embedded Swift, because introducing
|
|
// calls to generic deinit functions after the mandatory pipeline can result in IRGen crashes.
|
|
return true
|
|
}
|
|
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
|
|
}
|
|
|
|
if type.isBuiltinFixedArray {
|
|
return devirtualizeFixedSizeArray(destroy: destroy, isMandatory: isMandatory, context)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
private func devirtualizeFixedSizeArray(destroy: some DevirtualizableDestroy,
|
|
isMandatory: Bool,
|
|
_ context: some MutatingContext
|
|
) -> Bool {
|
|
guard let arraySize = destroy.type.builtinFixedArraySizeType.valueOfInteger else {
|
|
return false
|
|
}
|
|
let elementType = destroy.type.builtinFixedArrayElementType(in: destroy.parentFunction)
|
|
|
|
let destroyAddr = destroy.materializeAsDestroyAddr(context)
|
|
|
|
let builder = Builder(before: destroyAddr, context)
|
|
let base = builder.createVectorBaseAddr(vector: destroyAddr.destroyedAddress)
|
|
let count = builder.createIntegerLiteral(arraySize, type: context.getBuiltinWordType())
|
|
|
|
let success = createArrayDestroyLoop(baseAddress: base,
|
|
arrayCount: count,
|
|
elementType: elementType,
|
|
insertionPoint: destroyAddr,
|
|
isMandatory: isMandatory,
|
|
context)
|
|
|
|
context.erase(instruction: destroyAddr)
|
|
return success
|
|
}
|
|
|
|
// 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)
|
|
func materializeAsDestroyAddr(_ context: some MutatingContext) -> DestroyAddrInst
|
|
}
|
|
|
|
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 = deinitializer.isGeneric ? context.getContextSubstitutionMap(for: type) : SubstitutionMap()
|
|
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)
|
|
}
|
|
|
|
fileprivate func materializeAsDestroyAddr(_ context: some MutatingContext) -> DestroyAddrInst {
|
|
let builder = Builder(before: self, context)
|
|
let allocStack = builder.createAllocStack(destroyedValue.type)
|
|
builder.createStore(source: destroyedValue, destination: allocStack, ownership: .initialize)
|
|
let destroyAddr = builder.createDestroyAddr(address: allocStack)
|
|
builder.createDeallocStack(allocStack)
|
|
context.erase(instruction: self)
|
|
return destroyAddr
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
fileprivate func materializeAsDestroyAddr(_ context: some MutatingContext) -> DestroyAddrInst {
|
|
return self
|
|
}
|
|
}
|
|
|
|
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 || elementType.isBuiltinFixedArray)
|
|
else {
|
|
return true
|
|
}
|
|
|
|
// Lower the `builtin "destroyArray" to a loop which destroys all elements
|
|
|
|
let builder = Builder(before: builtinDestroyArray, context)
|
|
let baseAddress = builder.createPointerToAddress(pointer: builtinDestroyArray.arguments[1],
|
|
addressType: elementType.addressType,
|
|
isStrict: true, isInvariant: false)
|
|
|
|
let success = createArrayDestroyLoop(baseAddress: baseAddress,
|
|
arrayCount: builtinDestroyArray.arguments[2],
|
|
elementType: elementType,
|
|
insertionPoint: builtinDestroyArray,
|
|
isMandatory: isMandatory,
|
|
context)
|
|
|
|
context.erase(instruction: builtinDestroyArray)
|
|
return success
|
|
}
|
|
|
|
private func createArrayDestroyLoop(baseAddress: Value,
|
|
arrayCount: Value,
|
|
elementType: Type,
|
|
insertionPoint: Instruction,
|
|
isMandatory: Bool,
|
|
_ context: some MutatingContext
|
|
) -> Bool {
|
|
let indexType = arrayCount.type
|
|
let boolType = context.getBuiltinIntegerType(bitWidth: 1)
|
|
|
|
let preheaderBlock = insertionPoint.parentBlock
|
|
let exitBlock = context.splitBlock(after: insertionPoint)
|
|
let headerBlock = context.createBlock(after: preheaderBlock)
|
|
let bodyBlock = context.createBlock(after: headerBlock)
|
|
|
|
let preheaderBuilder = Builder(atEndOf: preheaderBlock, location: insertionPoint.location, context)
|
|
let zero = preheaderBuilder.createIntegerLiteral(0, type: indexType)
|
|
let one = preheaderBuilder.createIntegerLiteral(1, type: indexType)
|
|
let falseValue = preheaderBuilder.createBoolLiteral(false);
|
|
preheaderBuilder.createBranch(to: headerBlock, arguments: [zero])
|
|
|
|
let inductionVariable = headerBlock.addArgument(type: indexType, ownership: .none, context)
|
|
let headerBuilder = Builder(atEndOf: headerBlock, location: insertionPoint.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: insertionPoint.location, context)
|
|
let elementAddr = bodyBuilder.createIndexAddr(base: baseAddress, index: inductionVariable,
|
|
needStackProtection: false, isProjection: true)
|
|
let destroy = bodyBuilder.createDestroyAddr(address: elementAddr)
|
|
let resultType = context.getTupleType(elements: [indexType, boolType]).loweredType(in: insertionPoint.parentFunction)
|
|
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])
|
|
|
|
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)})
|
|
}
|
|
}
|