mirror of
https://github.com/apple/swift.git
synced 2025-12-21 12:14:44 +01:00
Optimizer: improve simplification of alloc_stack
* Reimplement most of the logic in Swift as an Instruction simplification and remove the old code from SILCombine
* support more cases of existential archetype replacements:
For example:
```
%0 = alloc_stack $any P
%1 = init_existential_addr %0, $T
use %1
```
is transformed to
```
%0 = alloc_stack $T
use %0
```
Also, if the alloc_stack is already an opened existential and the concrete type is known,
replace it as well:
```
%0 = metatype $@thick T.Type
%1 = init_existential_metatype %0, $@thick any P.Type
%2 = open_existential_metatype %1 : $@thick any P.Type to $@thick (@opened("X", P) Self).Type
...
%3 = alloc_stack $@opened("X", any P) Self
use %3
```
is transformed to
```
...
%3 = alloc_stack $T
use %3
```
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
|
||||
swift_compiler_sources(Optimizer
|
||||
SimplifyAllocRefDynamic.swift
|
||||
SimplifyAllocStack.swift
|
||||
SimplifyApply.swift
|
||||
SimplifyBeginAndLoadBorrow.swift
|
||||
SimplifyBeginCOWMutation.swift
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
//===--- SimplifyAllocStack.swift -----------------------------------------===//
|
||||
//
|
||||
// This source file is part of the Swift.org open source project
|
||||
//
|
||||
// Copyright (c) 2014 - 2025 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
|
||||
import AST
|
||||
|
||||
extension AllocStackInst : Simplifiable, SILCombineSimplifiable {
|
||||
func simplify(_ context: SimplifyContext) {
|
||||
if optimizeEnum(context) {
|
||||
return
|
||||
}
|
||||
_ = optimizeExistential(context)
|
||||
}
|
||||
}
|
||||
|
||||
private extension AllocStackInst {
|
||||
/// Replaces an alloc_stack of an enum by an alloc_stack of the payload if only one enum case (with payload)
|
||||
/// is stored to that location.
|
||||
///
|
||||
/// For example:
|
||||
/// ```
|
||||
/// %0 = alloc_stack $Optional<T>
|
||||
/// %1 = init_enum_data_addr %loc
|
||||
/// store %2 to %1
|
||||
/// ...
|
||||
/// %3 = unchecked_take_enum_data_addr %0
|
||||
/// %4 = load %3
|
||||
/// ```
|
||||
/// is transformed to
|
||||
/// ```
|
||||
/// %0 = alloc_stack $T
|
||||
/// store %2 to %0
|
||||
/// ...
|
||||
/// %4 = load %0
|
||||
/// ```
|
||||
func optimizeEnum(_ context: SimplifyContext) -> Bool {
|
||||
guard let (payloadType, isSingleInitTakePair) = getEnumInfo() else {
|
||||
return false
|
||||
}
|
||||
|
||||
let builder = Builder(before: self, context)
|
||||
let newAlloc = builder.createAllocStack(payloadType,
|
||||
hasDynamicLifetime: hasDynamicLifetime,
|
||||
isLexical: isLexical,
|
||||
isFromVarDecl: isFromVarDecl,
|
||||
usesMoveableValueDebugInfo: usesMoveableValueDebugInfo)
|
||||
let oldAllocType = type
|
||||
if let varInfo = debugVariable {
|
||||
builder.createDebugValue(value: Undef.get(type: oldAllocType, context), debugVariable: varInfo)
|
||||
}
|
||||
self.replace(with: newAlloc, context)
|
||||
|
||||
for use in newAlloc.uses {
|
||||
switch use.instruction {
|
||||
case let iea as InjectEnumAddrInst:
|
||||
context.erase(instruction: iea)
|
||||
case let da as DestroyAddrInst:
|
||||
if isSingleInitTakePair {
|
||||
// It's not possible that the enum has a payload at the destroy_addr, because it must have already
|
||||
// been taken by the take of the single init-take pair.
|
||||
// We _have_ to remove the destroy_addr, because we also remove all inject_enum_addrs which might
|
||||
// inject a payload-less case before the destroy_addr.
|
||||
// Otherwise the enum payload can still be valid at the destroy_addr, so we have to keep the destroy_addr.
|
||||
// Just replace the enum with the payload (and because it's not a singleInitTakePair, we can be sure
|
||||
// that the enum cannot have any other case than the payload case).
|
||||
context.erase(instruction: da)
|
||||
}
|
||||
case let ieda as InitEnumDataAddrInst:
|
||||
ieda.replace(with: newAlloc, context)
|
||||
case let uteda as UncheckedTakeEnumDataAddrInst:
|
||||
uteda.replace(with: newAlloc, context)
|
||||
case is DeallocStackInst:
|
||||
break
|
||||
case let dv as DebugValueInst:
|
||||
// TODO: Add support for op_enum_fragment
|
||||
dv.operand.set(to: Undef.get(type: oldAllocType, context), context)
|
||||
default:
|
||||
fatalError("unexpected alloc_stack user");
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func getEnumInfo() -> (payloadType: Type, isSingleInitTakePair: Bool)? {
|
||||
if !type.isEnum {
|
||||
return nil
|
||||
}
|
||||
var numInits = 0
|
||||
var numTakes = 0
|
||||
var initBlock: BasicBlock? = nil
|
||||
var takeBlock: BasicBlock? = nil
|
||||
var caseIndex: Int? = nil
|
||||
var payloadType: Type? = nil
|
||||
for use in uses {
|
||||
switch use.instruction {
|
||||
case is DestroyAddrInst,
|
||||
is DeallocStackInst,
|
||||
is DebugValueInst,
|
||||
// We'll check init_enum_addr below.
|
||||
is InjectEnumAddrInst:
|
||||
break
|
||||
case let ieda as InitEnumDataAddrInst:
|
||||
if let previouslyFoundCase = caseIndex, previouslyFoundCase != ieda.caseIndex {
|
||||
return nil
|
||||
}
|
||||
caseIndex = ieda.caseIndex
|
||||
assert(payloadType == nil || payloadType! == ieda.type)
|
||||
payloadType = ieda.type
|
||||
numInits += 1
|
||||
initBlock = ieda.parentBlock
|
||||
case let uted as UncheckedTakeEnumDataAddrInst:
|
||||
if let previouslyFoundCase = caseIndex, previouslyFoundCase != uted.caseIndex {
|
||||
return nil
|
||||
}
|
||||
caseIndex = uted.caseIndex
|
||||
numTakes += 1
|
||||
takeBlock = uted.parentBlock
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
guard let caseIndex, let payloadType else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the enum has a single init-take pair in a single block, we know that the enum cannot contain any
|
||||
// valid payload outside that init-take pair.
|
||||
//
|
||||
// This also means that we can ignore any inject_enum_addr of another enum case, because this can only
|
||||
// inject a case without a payload.
|
||||
if numInits == 1 && numTakes == 1 && initBlock == takeBlock {
|
||||
return (payloadType, isSingleInitTakePair: true)
|
||||
}
|
||||
// No single init-take pair: We cannot ignore inject_enum_addrs with a mismatching case.
|
||||
if uses.users(ofType: InjectEnumAddrInst.self).contains(where: { $0.caseIndex != caseIndex}) {
|
||||
return nil
|
||||
}
|
||||
return (payloadType, isSingleInitTakePair: false)
|
||||
}
|
||||
|
||||
/// Replaces an alloc_stack of an existential by an alloc_stack of the concrete type.
|
||||
///
|
||||
/// For example:
|
||||
/// ```
|
||||
/// %0 = alloc_stack $any P
|
||||
/// %1 = init_existential_addr %0, $T
|
||||
/// use %1
|
||||
/// ```
|
||||
/// is transformed to
|
||||
/// ```
|
||||
/// %0 = alloc_stack $T
|
||||
/// use %0
|
||||
/// ```
|
||||
///
|
||||
/// Also, if the alloc_stack is already an opened existential and the concrete type is known,
|
||||
/// replace it as well:
|
||||
/// ```
|
||||
/// %0 = metatype $@thick T.Type
|
||||
/// %1 = init_existential_metatype %0, $@thick any P.Type
|
||||
/// %2 = open_existential_metatype %1 : $@thick any P.Type to $@thick (@opened("X", P) Self).Type
|
||||
/// ...
|
||||
/// %3 = alloc_stack $@opened("X", any P) Self
|
||||
/// use %3
|
||||
/// ```
|
||||
/// is transformed to
|
||||
/// ```
|
||||
/// ...
|
||||
/// %3 = alloc_stack $T
|
||||
/// use %3
|
||||
/// ```
|
||||
func optimizeExistential(_ context: SimplifyContext) -> Bool {
|
||||
guard type.astType.isExistential || type.astType.isExistentialArchetype,
|
||||
let concreteFormalType = getConcreteTypeOfExistential()
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
let builder = Builder(before: self, context)
|
||||
let newAlloc = builder.createAllocStack(concreteFormalType.loweredType(in: parentFunction),
|
||||
hasDynamicLifetime: hasDynamicLifetime,
|
||||
isLexical: isLexical,
|
||||
isFromVarDecl: isFromVarDecl,
|
||||
usesMoveableValueDebugInfo: usesMoveableValueDebugInfo)
|
||||
for use in uses {
|
||||
switch use.instruction {
|
||||
case let dea as DeinitExistentialAddrInst:
|
||||
context.erase(instruction: dea)
|
||||
case let iea as InitExistentialAddrInst:
|
||||
if iea.type != newAlloc.type {
|
||||
// We need a cast if the concrete type of the init_existential_addr is itself an opened existential
|
||||
// for which we know the concrete type (which is differnt).
|
||||
let builder = Builder(before: iea, context)
|
||||
let addrCast = builder.createUncheckedAddrCast(from: newAlloc, to: iea.type)
|
||||
iea.replace(with: addrCast, context)
|
||||
} else {
|
||||
iea.replace(with: newAlloc, context)
|
||||
}
|
||||
case let oea as OpenExistentialAddrInst:
|
||||
assert(oea.uses.ignoreUsers(ofType: DestroyAddrInst.self).isEmpty)
|
||||
oea.replace(with: newAlloc, context)
|
||||
case let cab as CheckedCastAddrBranchInst:
|
||||
let builder = Builder(before: cab, context)
|
||||
builder.createCheckedCastAddrBranch(
|
||||
source: newAlloc, sourceFormalType: concreteFormalType,
|
||||
destination: cab.destination, targetFormalType: cab.targetFormalType,
|
||||
consumptionKind: cab.consumptionKind,
|
||||
successBlock: cab.successBlock, failureBlock: cab.failureBlock)
|
||||
context.erase(instruction: cab)
|
||||
case let ucca as UnconditionalCheckedCastAddrInst:
|
||||
let builder = Builder(before: ucca, context)
|
||||
builder.createUnconditionalCheckedCastAddr(
|
||||
source: newAlloc, sourceFormalType: concreteFormalType,
|
||||
destination: ucca.destination, targetFormalType: ucca.targetFormalType)
|
||||
context.erase(instruction: ucca)
|
||||
default:
|
||||
use.set(to: newAlloc, context)
|
||||
}
|
||||
}
|
||||
context.erase(instruction: self)
|
||||
return true
|
||||
}
|
||||
|
||||
// Returns the concrete type of this alloc_stack if known.
|
||||
// Assuming that its type is either an existential or an opened existential.
|
||||
private func getConcreteTypeOfExistential() -> CanonicalType? {
|
||||
var initExistential: InitExistentialAddrInst? = nil
|
||||
var requiresLegalFormalType = false
|
||||
|
||||
for use in uses {
|
||||
switch use.instruction {
|
||||
case is DestroyAddrInst,
|
||||
is DeinitExistentialAddrInst,
|
||||
is DeallocStackInst,
|
||||
is DebugValueInst:
|
||||
break
|
||||
case let oea as OpenExistentialAddrInst:
|
||||
if !oea.uses.ignoreUsers(ofType: DestroyAddrInst.self).isEmpty {
|
||||
return nil
|
||||
}
|
||||
case let iea as InitExistentialAddrInst:
|
||||
if initExistential != nil {
|
||||
return nil
|
||||
}
|
||||
initExistential = iea
|
||||
case is CheckedCastAddrBranchInst, is UnconditionalCheckedCastAddrInst:
|
||||
// To construct a new cast instruction we need a formal type.
|
||||
requiresLegalFormalType = true
|
||||
fallthrough
|
||||
case is UncheckedAddrCastInst:
|
||||
if use != use.instruction.operands[0] {
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
let concreteType: CanonicalType
|
||||
if let initExistential {
|
||||
assert(self.type.astType.isExistential)
|
||||
if let cft = initExistential.concreteTypeOfDependentExistentialArchetype {
|
||||
// Case 1: We will replace the alloc_stack of an existential with the concrete type.
|
||||
// `alloc_stack $any P` -> `alloc_stack $ConcreteType`
|
||||
concreteType = cft
|
||||
} else {
|
||||
// The instruction which defines the existential archetype must dominate the alloc_stack, because
|
||||
// after the transformation the alloc_stack will use the existential archetype.
|
||||
for openArchetypeOp in initExistential.typeDependentOperands {
|
||||
if !openArchetypeOp.value.definingInstruction!.dominatesInSameBlock(self) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// Case 2: We will replace the alloc_stack of an existential with the existential archetype.
|
||||
// `alloc_stack $any P` -> `alloc_stack $@opened("...")`
|
||||
concreteType = initExistential.type.astType
|
||||
}
|
||||
} else if self.type.astType.isExistentialArchetype, let cft = self.concreteTypeOfDependentExistentialArchetype {
|
||||
// Case 3: We will replace the alloc_stack of an existential archetype with the concrete type:
|
||||
// `alloc_stack $@opened("...")` -> `alloc_stack $ConcreteType`
|
||||
concreteType = cft
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
if requiresLegalFormalType && !concreteType.isLegalFormalType {
|
||||
return nil
|
||||
}
|
||||
return concreteType
|
||||
}
|
||||
}
|
||||
@@ -121,6 +121,7 @@ private func registerSwiftPasses() {
|
||||
registerForSILCombine(UncheckedEnumDataInst.self, { run(UncheckedEnumDataInst.self, $0) })
|
||||
registerForSILCombine(WitnessMethodInst.self, { run(WitnessMethodInst.self, $0) })
|
||||
registerForSILCombine(UnconditionalCheckedCastInst.self, { run(UnconditionalCheckedCastInst.self, $0) })
|
||||
registerForSILCombine(AllocStackInst.self, { run(AllocStackInst.self, $0) })
|
||||
registerForSILCombine(ApplyInst.self, { run(ApplyInst.self, $0) })
|
||||
registerForSILCombine(TryApplyInst.self, { run(TryApplyInst.self, $0) })
|
||||
|
||||
|
||||
Reference in New Issue
Block a user