Skip to content

Optimizer: replace PredictableMemoryAccessOptimizations with a "mandatory" redundant load elimination pass #79186

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Feb 8, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -63,21 +63,33 @@ import SIL
///
let redundantLoadElimination = FunctionPass(name: "redundant-load-elimination") {
(function: Function, context: FunctionPassContext) in
eliminateRedundantLoads(in: function, ignoreArrays: false, context)
_ = eliminateRedundantLoads(in: function, variant: .regular, context)
}

// Early RLE does not touch loads from Arrays. This is important because later array optimizations,
// like ABCOpt, get confused if an array load in a loop is converted to a pattern with a phi argument.
let earlyRedundantLoadElimination = FunctionPass(name: "early-redundant-load-elimination") {
(function: Function, context: FunctionPassContext) in
eliminateRedundantLoads(in: function, ignoreArrays: true, context)
_ = eliminateRedundantLoads(in: function, variant: .early, context)
}

private func eliminateRedundantLoads(in function: Function, ignoreArrays: Bool, _ context: FunctionPassContext) {
let mandatoryRedundantLoadElimination = FunctionPass(name: "mandatory-redundant-load-elimination") {
(function: Function, context: FunctionPassContext) in
_ = eliminateRedundantLoads(in: function, variant: .mandatory, context)
}

enum RedundantLoadEliminationVariant {
case mandatory, mandatoryInGlobalInit, early, regular
}

func eliminateRedundantLoads(in function: Function,
variant: RedundantLoadEliminationVariant,
_ context: FunctionPassContext) -> Bool
{
// Avoid quadratic complexity by limiting the number of visited instructions.
// This limit is sufficient for most "real-world" functions, by far.
var complexityBudget = 50_000
var changed = false

for block in function.blocks.reversed() {

Expand All @@ -89,50 +101,76 @@ private func eliminateRedundantLoads(in function: Function, ignoreArrays: Bool,

if let load = inst as? LoadInst {
if !context.continueWithNextSubpassRun(for: load) {
return
return changed
}
if ignoreArrays,
let nominal = load.type.nominal,
nominal == context.swiftArrayDecl
{
continue
if complexityBudget < 20 {
complexityBudget = 20
}
// Check if the type can be expanded without a significant increase to
// code size.
// We block redundant load elimination because it might increase
// register pressure for large values. Furthermore, this pass also
// splits values into its projections (e.g
// shrinkMemoryLifetimeAndSplit).
if !load.type.shouldExpand(context) {
continue
if !load.isEligibleForElimination(in: variant, context) {
continue;
}
tryEliminate(load: load, complexityBudget: &complexityBudget, context)
changed = tryEliminate(load: load, complexityBudget: &complexityBudget, context) || changed
}
}
}
return changed
}

private func tryEliminate(load: LoadInst, complexityBudget: inout Int, _ context: FunctionPassContext) {
private func tryEliminate(load: LoadInst, complexityBudget: inout Int, _ context: FunctionPassContext) -> Bool {
switch load.isRedundant(complexityBudget: &complexityBudget, context) {
case .notRedundant:
break
return false
case .redundant(let availableValues):
replace(load: load, with: availableValues, context)
return true
case .maybePartiallyRedundant(let subPath):
// Check if the a partial load would really be redundant to avoid unnecessary splitting.
switch load.isRedundant(at: subPath, complexityBudget: &complexityBudget, context) {
case .notRedundant, .maybePartiallyRedundant:
break
return false
case .redundant:
// The new individual loads are inserted right before the current load and
// will be optimized in the following loop iterations.
load.trySplit(context)
return load.trySplit(context)
}
}
}

private extension LoadInst {

func isEligibleForElimination(in variant: RedundantLoadEliminationVariant, _ context: FunctionPassContext) -> Bool {
switch variant {
case .mandatory, .mandatoryInGlobalInit:
if loadOwnership == .take {
// load [take] would require to shrinkMemoryLifetime. But we don't want to do this in the mandatory
// pipeline to not shrink or remove an alloc_stack which is relevant for debug info.
return false
}
switch address.accessBase {
case .box, .stack:
break
default:
return false
}
case .early:
// See the comment of `earlyRedundantLoadElimination`.
if let nominal = self.type.nominal, nominal == context.swiftArrayDecl {
return false
}
case .regular:
break
}
// Check if the type can be expanded without a significant increase to code size.
// We block redundant load elimination because it might increase register pressure for large values.
// Furthermore, this pass also splits values into its projections (e.g shrinkMemoryLifetimeAndSplit).
// But: it is required to remove loads, even of large structs, in global init functions to ensure
// that globals (containing large structs) can be statically initialized.
if variant != .mandatoryInGlobalInit, !self.type.shouldExpand(context) {
return false
}
return true
}

enum DataflowResult {
case notRedundant
case redundant([AvailableValue])
Expand Down Expand Up @@ -251,7 +289,7 @@ private func replace(load: LoadInst, with availableValues: [AvailableValue], _ c
var ssaUpdater = SSAUpdater(function: load.parentFunction,
type: load.type, ownership: load.ownership, context)

for availableValue in availableValues {
for availableValue in availableValues.replaceCopyAddrsWithLoadsAndStores(context) {
let block = availableValue.instruction.parentBlock
let availableValue = provideValue(for: load, from: availableValue, context)
ssaUpdater.addAvailableValue(availableValue, in: block)
Expand Down Expand Up @@ -279,6 +317,10 @@ private func replace(load: LoadInst, with availableValues: [AvailableValue], _ c
//
newValue = ssaUpdater.getValue(inMiddleOf: load.parentBlock)
}

// Make sure to keep dependencies valid after replacing the load
insertMarkDependencies(for: load, context)

load.replace(with: newValue, context)
}

Expand Down Expand Up @@ -306,6 +348,39 @@ private func provideValue(
}
}

/// If the memory location depends on something, insert a dependency for the loaded value:
///
/// %2 = mark_dependence %1 on %0
/// %3 = load %2
/// ->
/// %2 = mark_dependence %1 on %0 // not needed anymore, can be removed eventually
/// %3 = load %2
/// %4 = mark_dependence %3 on %0
/// // replace %3 with %4
///
private func insertMarkDependencies(for load: LoadInst, _ context: FunctionPassContext) {
var inserter = MarkDependenceInserter(load: load, context: context)
_ = inserter.walkUp(address: load.address, path: UnusedWalkingPath())
}

private struct MarkDependenceInserter : AddressUseDefWalker {
let load: LoadInst
let context: FunctionPassContext

mutating func walkUp(address: Value, path: UnusedWalkingPath) -> WalkResult {
if let mdi = address as? MarkDependenceInst {
let builder = Builder(after: load, context)
let newMdi = builder.createMarkDependence(value: load, base: mdi.base, kind: mdi.dependenceKind)
load.uses.ignore(user: newMdi).replaceAll(with: newMdi, context)
}
return walkUpDefault(address: address, path: path)
}

mutating func rootDef(address: Value, path: UnusedWalkingPath) -> WalkResult {
return .continueWalk
}
}

/// In case of a `load [take]` shrink lifetime of the value in memory back to the `availableValue`
/// and return the (possibly projected) available value. For example:
///
Expand Down Expand Up @@ -342,6 +417,8 @@ private func shrinkMemoryLifetime(from load: LoadInst, to availableValue: Availa
fatalError("unqualified store in ossa function?")
}
return valueToAdd
case .viaCopyAddr:
fatalError("copy_addr must be lowered before shrinking lifetime")
}
}

Expand Down Expand Up @@ -380,39 +457,58 @@ private func shrinkMemoryLifetimeAndSplit(from load: LoadInst, to availableValue
let valueToAdd = builder.createLoad(fromAddress: addr, ownership: .take)
availableStore.trySplit(context)
return valueToAdd
case .viaCopyAddr:
fatalError("copy_addr must be lowered before shrinking lifetime")
}
}

/// Either a `load` or `store` which is preceding the original load and provides the loaded value.
private enum AvailableValue {
case viaLoad(LoadInst)
case viaStore(StoreInst)
case viaCopyAddr(CopyAddrInst)

var value: Value {
switch self {
case .viaLoad(let load): return load
case .viaStore(let store): return store.source
case .viaCopyAddr: fatalError("copy_addr must be lowered")
}
}

var address: Value {
switch self {
case .viaLoad(let load): return load.address
case .viaStore(let store): return store.destination
case .viaLoad(let load): return load.address
case .viaStore(let store): return store.destination
case .viaCopyAddr(let copyAddr): return copyAddr.destination
}
}

var instruction: Instruction {
switch self {
case .viaLoad(let load): return load
case .viaStore(let store): return store
case .viaLoad(let load): return load
case .viaStore(let store): return store
case .viaCopyAddr(let copyAddr): return copyAddr
}
}

func getBuilderForProjections(_ context: FunctionPassContext) -> Builder {
switch self {
case .viaLoad(let load): return Builder(after: load, context)
case .viaStore(let store): return Builder(before: store, context)
case .viaCopyAddr: fatalError("copy_addr must be lowered")
}
}
}

private extension Array where Element == AvailableValue {
func replaceCopyAddrsWithLoadsAndStores(_ context: FunctionPassContext) -> [AvailableValue] {
return map {
if case .viaCopyAddr(let copyAddr) = $0 {
return .viaStore(copyAddr.replaceWithLoadAndStore(context))
} else {
return $0
}
}
}
}
Expand Down Expand Up @@ -479,7 +575,7 @@ private struct InstructionScanner {

private mutating func visit(instruction: Instruction) -> ScanResult {
switch instruction {
case is FixLifetimeInst, is EndAccessInst, is EndBorrowInst:
case is FixLifetimeInst, is BeginAccessInst, is EndAccessInst, is EndBorrowInst:
// Those scope-ending instructions are only irrelevant if the preceding load is not changed.
// If it is changed from `load [copy]` -> `load [take]` the memory effects of those scope-ending
// instructions prevent that the `load [take]` will illegally mutate memory which is protected
Expand Down Expand Up @@ -520,6 +616,16 @@ private struct InstructionScanner {
potentiallyRedundantSubpath = precedingStorePath
}

case let preceedingCopy as CopyAddrInst where preceedingCopy.canProvideValue:
let copyPath = preceedingCopy.destination.constantAccessPath
if copyPath.getMaterializableProjection(to: accessPath) != nil {
availableValues.append(.viaCopyAddr(preceedingCopy))
return .available
}
if accessPath.getMaterializableProjection(to: copyPath) != nil, potentiallyRedundantSubpath == nil {
potentiallyRedundantSubpath = copyPath
}

default:
break
}
Expand Down Expand Up @@ -606,3 +712,20 @@ private struct Liverange {
return false
}
}

private extension CopyAddrInst {
var canProvideValue: Bool {
if !source.type.isLoadable(in: parentFunction) {
// Although the original load's type is loadable (obviously), it can be projected-out
// from the copy_addr's type which might be not loadable.
return false
}
if !parentFunction.hasOwnership {
if !isTakeOfSrc || !isInitializationOfDest {
// For simplicity, bail if we would have to insert compensating retains and releases.
return false
}
}
return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,14 @@ private func optimize(function: Function, _ context: FunctionPassContext, _ modu
{
fri.referencedFunction.set(linkage: .public, moduleContext)
}

case let copy as CopyAddrInst:
if function.isGlobalInitOnceFunction, copy.source.type.isLoadable(in: function) {
// In global init functions we have to make sure that redundant load elimination can remove all
// loads (from temporary stack locations) so that globals can be statically initialized.
// For this it's necessary to load copy_addr instructions to loads and stores.
copy.replaceWithLoadAndStore(simplifyCtxt)
}

default:
break
Expand All @@ -170,7 +178,13 @@ private func optimize(function: Function, _ context: FunctionPassContext, _ modu
removeUnusedMetatypeInstructions(in: function, context)

// If this is a just specialized function, try to optimize copy_addr, etc.
changed = context.optimizeMemoryAccesses(in: function) || changed
if eliminateRedundantLoads(in: function,
variant: function.isGlobalInitOnceFunction ? .mandatoryInGlobalInit : .mandatory,
context)
{
changed = true
}

changed = context.eliminateDeadAllocations(in: function) || changed
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,14 +346,6 @@ struct FunctionPassContext : MutatingContext {
_bridged.asNotificationHandler().notifyChanges(.effectsChanged)
}

func optimizeMemoryAccesses(in function: Function) -> Bool {
if _bridged.optimizeMemoryAccesses(function.bridged) {
notifyInstructionsChanged()
return true
}
return false
}

func eliminateDeadAllocations(in function: Function) -> Bool {
if _bridged.eliminateDeadAllocations(function.bridged) {
notifyInstructionsChanged()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ private func registerSwiftPasses() {
registerPass(stripObjectHeadersPass, { stripObjectHeadersPass.run($0) })
registerPass(deadStoreElimination, { deadStoreElimination.run($0) })
registerPass(redundantLoadElimination, { redundantLoadElimination.run($0) })
registerPass(mandatoryRedundantLoadElimination, { mandatoryRedundantLoadElimination.run($0) })
registerPass(earlyRedundantLoadElimination, { earlyRedundantLoadElimination.run($0) })
registerPass(deinitDevirtualizer, { deinitDevirtualizer.run($0) })
registerPass(lifetimeDependenceDiagnosticsPass, { lifetimeDependenceDiagnosticsPass.run($0) })
Expand Down
Loading