mirror of
https://github.com/apple/swift.git
synced 2025-12-14 20:36:38 +01:00
Specifically, improved debug info retention in: * tryReplaceRedundantInstructionPair, * splitAggregateLoad, * TempLValueElimination, * Mem2Reg, * ConstantFolding. The changes to Mem2Reg allow debug info to be retained in the case tested by self-nostorage.swift in -O builds, so we have just enabled -O in that file instead of writing a new test for it. We attempted to add a case to salvageDebugInfo for unchecked_enum_data, but it caused crashes in Linux CI that we were not able to reproduce.
322 lines
12 KiB
Swift
322 lines
12 KiB
Swift
//===--- TempLValueElimination.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 AST
|
|
import SIL
|
|
|
|
/// Eliminates copies from a temporary (an "l-value") to a destination.
|
|
///
|
|
/// ```
|
|
/// %temp = alloc_stack $T
|
|
/// ... -+
|
|
/// store %x to %temp | no reads or writes to %destination
|
|
/// ... -+
|
|
/// copy_addr [take] %temp to [init] %destination
|
|
/// dealloc_stack %temp
|
|
/// ```
|
|
/// ->
|
|
/// ```
|
|
/// ...
|
|
/// store %x to %destination
|
|
/// ...
|
|
/// ```
|
|
///
|
|
/// The name TempLValueElimination refers to the TempRValueElimination pass, which performs
|
|
/// a related transformation, just with the temporary on the "right" side.
|
|
///
|
|
/// The pass also performs a peephole optimization on `copy_addr` - `destroy_addr` sequences.
|
|
/// It replaces
|
|
///
|
|
/// ```
|
|
/// copy_addr %source to %destination
|
|
/// destroy_addr %source
|
|
/// ```
|
|
/// ->
|
|
/// ```
|
|
/// copy_addr [take] %source to %destination
|
|
/// ```
|
|
///
|
|
let tempLValueElimination = FunctionPass(name: "temp-lvalue-elimination") {
|
|
(function: Function, context: FunctionPassContext) in
|
|
|
|
for inst in function.instructions {
|
|
switch inst {
|
|
case let copy as CopyAddrInst:
|
|
combineWithDestroy(copy: copy, context)
|
|
tryEliminate(copy: copy, context)
|
|
case let store as StoreInst:
|
|
// Also handle `load`-`store` pairs which are basically the same thing as a `copy_addr`.
|
|
if let load = store.source as? LoadInst, load.uses.isSingleUse, load.parentBlock == store.parentBlock {
|
|
tryEliminate(copy: store, context)
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
private func tryEliminate(copy: CopyLikeInstruction, _ context: FunctionPassContext) {
|
|
guard let allocStack = copy.sourceAddress as? AllocStackInst,
|
|
allocStack.isDeallocatedInSameBlock(as: copy)
|
|
else {
|
|
return
|
|
}
|
|
let isTrivial = allocStack.type.isTrivial(in: copy.parentFunction)
|
|
guard copy.isTakeOfSource || isTrivial else {
|
|
return
|
|
}
|
|
|
|
// We need to move all destination address projections at the begin of the alloc_stack liverange,
|
|
// because we are replacing the alloc_stack uses with the destination.
|
|
// ```
|
|
// %destination = struct_element_addr %1
|
|
// stores to %temp --> stores to %destination
|
|
// %destination = struct_element_addr %1
|
|
// copy_addr [take] %temp to %destination
|
|
// ```
|
|
var projections = InstructionSet(context)
|
|
defer { projections.deinitialize() }
|
|
let destinationRootAddress = collectMovableProjections(of: copy.destinationAddress, in: &projections)
|
|
|
|
// If true we need to explicitly destroy the destination at the begin of the liverange.
|
|
// ```
|
|
// destroy_addr %destination
|
|
// stores to %temp --> stores to %destination
|
|
// copy_addr [take] %temp to %destination
|
|
// ```
|
|
let needDestroyEarly = !copy.isInitializationOfDestination && !isTrivial
|
|
|
|
let firstUseOfAllocStack = InstructionList(first: allocStack).first(where: { $0.isUsing(allocStack) }) ??
|
|
// The conservative default, if the fist use is not in the alloc_stack's block.
|
|
allocStack.parentBlock.terminator
|
|
|
|
if firstUseOfAllocStack == copy.loadingInstruction {
|
|
// The alloc_stack is not written yet at the point of the copy. This is a very unusual corner case
|
|
// which can only happen if the alloc_stack has an empty type (e.g. `$()`).
|
|
return
|
|
}
|
|
|
|
let aliasAnalysis = context.aliasAnalysis
|
|
let calleeAnalysis = context.calleeAnalysis
|
|
|
|
if aliasAnalysis.mayAlias(allocStack, copy.destinationAddress) {
|
|
// Catch the very unusual corner case where the copy is writing back to it's source address - the alloc_stack.
|
|
return
|
|
}
|
|
|
|
// If exclusivity is checked for the alloc_stack we must not replace it with the copy-destination.
|
|
// If the copy-destination is also in an access-scope this would result in an exclusivity violation
|
|
// which was not there before.
|
|
guard allocStack.uses.users(ofType: BeginAccessInst.self).isEmpty else {
|
|
return
|
|
}
|
|
|
|
var worklist = InstructionWorklist(context)
|
|
defer { worklist.deinitialize() }
|
|
worklist.pushIfNotVisited(firstUseOfAllocStack)
|
|
|
|
// Check instructions within the liverange of the alloc_stack.
|
|
while let inst = worklist.pop() {
|
|
// If the destination root address is within the liverange it would prevent moving the projections
|
|
// before the first use. Note that if the defining instruction of `destinationRootAddress` is nil
|
|
// it can only be a function argument.
|
|
if inst == destinationRootAddress.definingInstruction {
|
|
return
|
|
}
|
|
|
|
// Check if the destination is not accessed within the liverange of the temporary.
|
|
// This is unlikely, because the destination is initialized at the copy.
|
|
// But still, the destination could contain an initialized value which is destroyed before the copy.
|
|
if inst.mayReadOrWrite(address: copy.destinationAddress, aliasAnalysis) &&
|
|
// Needed to treat `init_existential_addr` as not-writing projection.
|
|
!projections.contains(inst)
|
|
{
|
|
return
|
|
}
|
|
|
|
// Check if replacing the alloc_stack with destination would invalidate the alias rules of indirect arguments.
|
|
if let apply = inst as? FullApplySite,
|
|
apply.hasInvalidArgumentAliasing(between: allocStack, and: copy.destinationAddress, aliasAnalysis)
|
|
{
|
|
return
|
|
}
|
|
|
|
// We must not shrink the liverange of an existing value in the destination.
|
|
if needDestroyEarly && inst.isDeinitBarrier(calleeAnalysis) {
|
|
return
|
|
}
|
|
|
|
worklist.pushSuccessors(of: inst, ignoring: copy)
|
|
}
|
|
|
|
if allocStack.isReadOrWritten(after: copy.loadingInstruction, aliasAnalysis) {
|
|
// Bail in the unlikely case of the alloc_stack is re-initialized after its value has been taken by `copy`.
|
|
return
|
|
}
|
|
|
|
moveProjections(of: copy.destinationAddress, within: worklist, before: firstUseOfAllocStack, context)
|
|
|
|
if needDestroyEarly {
|
|
// Make sure the destination is uninitialized before the liverange of the temporary.
|
|
let builder = Builder(before: firstUseOfAllocStack, context)
|
|
builder.createDestroyAddr(address: copy.destinationAddress)
|
|
}
|
|
|
|
// Replace all uses of the temporary with the destination address.
|
|
for use in allocStack.uses {
|
|
switch use.instruction {
|
|
case let deallocStack as DeallocStackInst:
|
|
context.erase(instruction: deallocStack)
|
|
default:
|
|
use.set(to: copy.destinationAddress, context)
|
|
}
|
|
}
|
|
|
|
// Salvage the debug variable attribute, if present.
|
|
if let debugVariable = allocStack.debugVariable {
|
|
let builder = Builder(before: firstUseOfAllocStack, location: allocStack.location, context)
|
|
builder.createDebugValue(value: copy.destinationAddress, debugVariable: debugVariable)
|
|
}
|
|
context.erase(instruction: allocStack)
|
|
context.erase(instructionIncludingAllUsers: copy.loadingInstruction)
|
|
}
|
|
|
|
private extension FullApplySite {
|
|
/// Returns true if after replacing `addr1` with `addr2` the apply would have invalid aliasing of
|
|
/// indirect arguments.
|
|
/// An indirect argument (except `@inout_aliasable`) must not alias with another indirect argument.
|
|
/// For example, if we would replace `addr1` with `addr2` in
|
|
/// ```
|
|
/// apply %f(%addr1, %addr2) : (@in T) -> @out T
|
|
/// ```
|
|
/// we would invalidate this rule.
|
|
func hasInvalidArgumentAliasing(between addr1: Value, and addr2: Value, _ aliasAnalysis: AliasAnalysis) -> Bool {
|
|
var addr1Accessed = false
|
|
var addr2Accessed = false
|
|
var mutatingAccess = false
|
|
for argOp in argumentOperands {
|
|
let convention = convention(of: argOp)!
|
|
if convention.isExclusiveIndirect {
|
|
if aliasAnalysis.mayAlias(addr1, argOp.value) {
|
|
addr1Accessed = true
|
|
if !convention.isGuaranteed {
|
|
mutatingAccess = true
|
|
}
|
|
} else if aliasAnalysis.mayAlias(addr2, argOp.value) {
|
|
addr2Accessed = true
|
|
if !convention.isGuaranteed {
|
|
mutatingAccess = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return mutatingAccess && addr1Accessed && addr2Accessed
|
|
}
|
|
}
|
|
|
|
/// Replace
|
|
/// ```
|
|
/// copy_addr %source to %destination --> copy_addr [take] %source to %destination
|
|
/// destroy_addr %source
|
|
/// ```
|
|
private func combineWithDestroy(copy: CopyAddrInst, _ context: FunctionPassContext) {
|
|
guard !copy.isTakeOfSource else {
|
|
return
|
|
}
|
|
|
|
// Check if the destroy_addr is after the copy_addr and if there are no memory accesses between them.
|
|
var debugInsts = Stack<DebugValueInst>(context)
|
|
defer { debugInsts.deinitialize() }
|
|
|
|
for inst in InstructionList(first: copy.next) {
|
|
switch inst {
|
|
case let destroy as DestroyAddrInst where destroy.destroyedAddress == copy.source:
|
|
copy.set(isTakeOfSource: true, context)
|
|
context.erase(instruction: destroy)
|
|
// Don't let debug info think that the value is still valid after the `copy [take]`.
|
|
context.erase(instructions: debugInsts)
|
|
return
|
|
case let debugInst as DebugValueInst where debugInst.operand.value == copy.source:
|
|
debugInsts.append(debugInst)
|
|
default:
|
|
if inst.mayReadOrWriteMemory {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension Value {
|
|
var isMovableProjection: (SingleValueInstruction & UnaryInstruction)? {
|
|
switch self {
|
|
case let projectionInst as InitEnumDataAddrInst: return projectionInst
|
|
case let projectionInst as StructElementAddrInst: return projectionInst
|
|
case let projectionInst as TupleElementAddrInst: return projectionInst
|
|
case let projectionInst as UncheckedTakeEnumDataAddrInst: return projectionInst
|
|
case let projectionInst as InitExistentialAddrInst: return projectionInst
|
|
case let projectionInst as RefElementAddrInst: return projectionInst
|
|
case let projectionInst as RefTailAddrInst: return projectionInst
|
|
case let projectionInst as ProjectBoxInst: return projectionInst
|
|
default: return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
private func collectMovableProjections(of address: Value, in projections: inout InstructionSet) -> Value {
|
|
var a = address
|
|
while let projection = a.isMovableProjection {
|
|
projections.insert(projection)
|
|
a = projection.operand.value
|
|
}
|
|
return a
|
|
}
|
|
|
|
private func moveProjections(
|
|
of address: Value,
|
|
within worklist: InstructionWorklist,
|
|
before insertionPoint: Instruction,
|
|
_ context: FunctionPassContext
|
|
) {
|
|
var a = address
|
|
var ip = insertionPoint
|
|
while let projection = a.isMovableProjection,
|
|
worklist.hasBeenPushed(projection)
|
|
{
|
|
projection.move(before: ip, context)
|
|
a = projection.operand.value
|
|
ip = projection
|
|
}
|
|
}
|
|
|
|
private extension AllocStackInst {
|
|
func isReadOrWritten(after afterInst: Instruction, _ aliasAnalysis: AliasAnalysis) -> Bool {
|
|
for inst in InstructionList(first: afterInst.next) {
|
|
if let deallocStack = inst as? DeallocStackInst, deallocStack.allocatedValue == self {
|
|
return false
|
|
}
|
|
if inst.mayReadOrWrite(address: self, aliasAnalysis) {
|
|
return true
|
|
}
|
|
}
|
|
fatalError("dealloc_stack expected to be in same block as `afterInst`")
|
|
}
|
|
|
|
func isDeallocatedInSameBlock(as inst: Instruction) -> Bool {
|
|
if let deallocStack = uses.users(ofType: DeallocStackInst.self).singleElement,
|
|
deallocStack.parentBlock == inst.parentBlock
|
|
{
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
}
|