mirror of
https://github.com/apple/swift.git
synced 2025-12-21 12:14:44 +01:00
This set of utilities introduce concepts such as `AccessBase`, `AccessPath` and `AccessStoragePath` useful to analyze memory accesses.
515 lines
18 KiB
Swift
515 lines
18 KiB
Swift
//===--- AccessUtils.swift - Utilities for analyzing memory accesses ------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2014 - 2022 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
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
// This file provides a set of utilities for analyzing memory accesses.
|
|
// It defines the following concepts
|
|
// - `AccessBase`: represents the base address of a memory access.
|
|
// - `AccessPath`: a pair of an `AccessBase` and `SmallProjectionPath` with the
|
|
// the path describing the specific address (in terms of projections) of the
|
|
// access.
|
|
// - `AccessStoragePath`: identifies the reference (or a value which contains a
|
|
// reference) an address originates from.
|
|
//
|
|
// The snippet below shows the relationship between the access concepts.
|
|
// ```
|
|
// %ref = struct_extract %value, #f1 AccessStoragePath
|
|
// %base = ref_element_addr %ref, #f2 AccessBase AccessPath |
|
|
// %scope = begin_access %base AccessScope | |
|
|
// %t = tuple_element_addr %scope, 0 | |
|
|
// %s = struct_element_addr %t, #f3 v v
|
|
// %l = load %s the access
|
|
// ```
|
|
//===----------------------------------------------------------------------===//
|
|
import SIL
|
|
|
|
/// "AccessBase describes the base address of a memory access (e.g. of a `load` or `store``).
|
|
/// The access address is either the base address directly or an address
|
|
/// projection of it.
|
|
/// The following snippets show examples of memory accesses and their respective bases.
|
|
///
|
|
/// ```
|
|
/// %base1 = ref_element_addr %ref, #Obj.field
|
|
/// %base2 = alloc_stack $S
|
|
/// %base3 = global_addr @gaddr
|
|
/// %addr1 = struct_element_addr %base1
|
|
/// %access1 = store %v1 to [trivial] %addr1 // accessed address is offset from base
|
|
/// %access2 = store %v2 to [trivial] %base2 // accessed address is base itself
|
|
/// ```
|
|
///
|
|
/// The base address is never inside an access scope.
|
|
struct AccessBase : CustomStringConvertible {
|
|
|
|
public enum Kind {
|
|
case box
|
|
case stack
|
|
case global
|
|
case `class`
|
|
case tail
|
|
case argument
|
|
case yield
|
|
case pointer
|
|
|
|
var isObject: Bool { self == .class || self == .tail }
|
|
}
|
|
|
|
let baseAddress: Value
|
|
let kind: Kind
|
|
|
|
init?(baseAddress: Value) {
|
|
switch baseAddress {
|
|
case is RefElementAddrInst : kind = .class
|
|
case is RefTailAddrInst : kind = .tail
|
|
case is ProjectBoxInst : kind = .box
|
|
case is AllocStackInst : kind = .stack
|
|
case is FunctionArgument : kind = .argument
|
|
case is GlobalAddrInst : kind = .global
|
|
default:
|
|
if baseAddress.definingInstruction is BeginApplyInst &&
|
|
baseAddress.type.isAddress {
|
|
kind = .yield
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
self.baseAddress = baseAddress
|
|
}
|
|
|
|
init(baseAddress: Value, kind: Kind) {
|
|
self.baseAddress = baseAddress
|
|
self.kind = kind
|
|
}
|
|
|
|
var description: String {
|
|
"\(kind) - \(baseAddress)"
|
|
}
|
|
|
|
/// Returns `true` if this is an access to a class instance
|
|
var isObjectAccess: Bool {
|
|
return kind == .class || kind == .tail
|
|
}
|
|
|
|
/// Returns a value of reference type if this is a reference projection (class, box, tail)
|
|
var reference: Value? {
|
|
switch baseAddress {
|
|
case let rea as RefElementAddrInst:
|
|
return rea.operand
|
|
case let pb as ProjectBoxInst:
|
|
return pb.operand
|
|
case let rta as RefTailAddrInst:
|
|
return rta.operand
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if the baseAddress is of an immutable property/global variable
|
|
var isLet: Bool {
|
|
switch baseAddress {
|
|
case let rea as RefElementAddrInst:
|
|
return rea.fieldIsLet
|
|
case let ga as GlobalAddrInst:
|
|
return ga.global.isLet
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if the address is immediately produced by a stack or box allocation
|
|
var isLocal: Bool {
|
|
switch kind {
|
|
case .box:
|
|
// The operand of the projection can be an argument, in which
|
|
// case it wouldn't be local
|
|
return (baseAddress as! ProjectBoxInst).operand is AllocBoxInst
|
|
case .class:
|
|
let op = (baseAddress as! RefElementAddrInst).operand
|
|
return op is AllocRefInst || op is AllocRefDynamicInst
|
|
case .stack:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if we can reliably compare this `AccessBase`
|
|
/// with another `AccessBase` for equality.
|
|
/// When comparing two uniquely identified access bases and they are not equal,
|
|
/// it follows that the accessed memory addresses do not alias.
|
|
/// This is e.g. not the case for class references: two different references
|
|
/// may still point to the same object.
|
|
var isUniquelyIdentified: Bool {
|
|
switch kind {
|
|
case .box:
|
|
// The operand `%op` in `%baseAddress = project_box %op` can
|
|
// be `alloc_box` or an argument. Only if it's a fresh allocation it is
|
|
// uniquelyIdentified, otherwise it is aliasable, as all the other references.
|
|
return (baseAddress as! ProjectBoxInst).operand is AllocBoxInst
|
|
case .stack, .global:
|
|
return true
|
|
case .argument:
|
|
// An argument address that is non-aliasable
|
|
return (baseAddress as! FunctionArgument).isExclusiveIndirectParameter
|
|
case .class:
|
|
let op = (baseAddress as! RefElementAddrInst).operand
|
|
return op is AllocRefInst || op is AllocRefDynamicInst
|
|
case .tail, .yield, .pointer:
|
|
// References (.class and .tail) may alias, and so do pointers and
|
|
// yield results
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if the two access bases do not alias
|
|
func isDistinct(from other: AccessBase) -> Bool {
|
|
switch (baseAddress, other.baseAddress) {
|
|
case is (AllocStackInst, AllocStackInst):
|
|
return baseAddress != other.baseAddress
|
|
case let (this as ProjectBoxInst, that as ProjectBoxInst)
|
|
where this.operand is AllocBoxInst && that.operand is AllocBoxInst:
|
|
return this.operand != that.operand
|
|
case let (this as GlobalAddrInst, that as GlobalAddrInst):
|
|
return this.global != that.global
|
|
case let (this as FunctionArgument, that as FunctionArgument):
|
|
return (this.isExclusiveIndirectParameter || that.isExclusiveIndirectParameter) && this != that
|
|
case let (this as RefElementAddrInst, that as RefElementAddrInst):
|
|
return (this.fieldIndex != that.fieldIndex)
|
|
default:
|
|
let selfIsUniquelyIdentified = isUniquelyIdentified
|
|
let otherIsUniquelyIdentified = other.isUniquelyIdentified
|
|
if selfIsUniquelyIdentified && otherIsUniquelyIdentified && kind != other.kind { return true }
|
|
// property: `isUniquelyIdentified` XOR `isObject`
|
|
if selfIsUniquelyIdentified && other.kind.isObject { return true }
|
|
if kind.isObject && otherIsUniquelyIdentified { return true }
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An `AccessPath` is a pair of a `base: AccessBase` and a `projectionPath: Path`
|
|
/// which denotes the offset of the access from the base in terms of projections.
|
|
struct AccessPath : CustomStringConvertible {
|
|
let base: AccessBase
|
|
|
|
/// address projections only
|
|
let projectionPath: SmallProjectionPath
|
|
|
|
var description: String {
|
|
"\(projectionPath): \(base)"
|
|
}
|
|
|
|
func isDistinct(from other: AccessPath) -> Bool {
|
|
return
|
|
base.isDistinct(from: other.base) || // The base is distinct, in which case we are done. OR
|
|
(base.baseAddress == other.base.baseAddress && // (The base is the exact same AND
|
|
!(projectionPath.matches(pattern: other.projectionPath) // the projection paths do not overlap)
|
|
|| (other.projectionPath.matches(pattern: projectionPath))))
|
|
}
|
|
}
|
|
|
|
/// An `AccessStoragePath` is the reference (or a value which contains a reference)
|
|
/// an address originates from.
|
|
/// In the following example the `storage` is `contains_ref` with `path` `"s0.c0.s0"`
|
|
/// ```
|
|
/// %ref = struct_extract %contains_ref : $S, #S.l
|
|
/// %base = ref_element_addr %ref : $List, #List.x
|
|
/// %addr = struct_element_addr %base : $X, #X.e
|
|
/// store %v to [trivial] %addr : $*Int
|
|
/// ```
|
|
struct AccessStoragePath {
|
|
let storage: Value
|
|
|
|
/// Only valid paths are: `"<sequence of value projections>.<one reference projection>.<sequence of address projections>"`
|
|
let path: SmallProjectionPath
|
|
}
|
|
|
|
private func canBeOperandOfIndexAddr(_ value: Value) -> Bool {
|
|
switch value {
|
|
case is IndexAddrInst, is RefTailAddrInst, is PointerToAddressInst:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
enum AddressOrPointerArgument {
|
|
case address(Value)
|
|
case pointer(FunctionArgument)
|
|
}
|
|
|
|
extension AddressOrPointerArgument : Equatable {
|
|
static func ==(lhs: AddressOrPointerArgument, rhs: AddressOrPointerArgument) -> Bool {
|
|
switch (lhs, rhs) {
|
|
case let (.address(left), .address(right)):
|
|
return left == right
|
|
case let (.pointer(left), .pointer(right)):
|
|
return left == right
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Given a `%addr = pointer_to_address %ptr_operand` instruction tries to identify
|
|
/// the address the pointer operand `ptr_operand` originates from, if any exists.
|
|
/// This is useful to identify patterns like
|
|
/// ```
|
|
/// %orig_addr = global_addr @...
|
|
/// %ptr = address_to_pointer %orig_addr
|
|
/// %addr = pointer_to_address %ptr
|
|
/// ```
|
|
/// which might arise when `[global_init]` functions for global addressors are inlined.
|
|
///
|
|
/// Alternatively, if the pointer originates from a ``FunctionArgument``, the argument is returned.
|
|
///
|
|
/// This underlying use-def traversal might cross phi arguments to identify the originating address
|
|
/// to handle diamond-shaped control-flow with common originating
|
|
/// address which might arise due to transformations ([example] (https://github.com/apple/swift/blob/8f9c5339542b17af9033f51ad7a0b95a043cad1b/test/SILOptimizer/access_storage_analysis_ossa.sil#L669-L705)) .
|
|
struct PointerIdentification {
|
|
private var walker = PointerIdentificationUseDefWalker()
|
|
|
|
mutating func getOriginatingAddressOrArgument(_ atp: PointerToAddressInst) -> AddressOrPointerArgument? {
|
|
walker.start(atp.type)
|
|
if walker.walkUp(value: atp.operand, path: SmallProjectionPath()) == .abortWalk {
|
|
return nil
|
|
}
|
|
return walker.result
|
|
}
|
|
|
|
private struct PointerIdentificationUseDefWalker : ValueUseDefWalker {
|
|
private var addrType: Type!
|
|
private(set) var result: AddressOrPointerArgument?
|
|
|
|
mutating func start(_ addrType: Type) {
|
|
self.addrType = addrType
|
|
result = nil
|
|
walkUpCache.clear()
|
|
}
|
|
|
|
mutating func rootDef(value: Value, path: SmallProjectionPath) -> WalkResult {
|
|
switch value {
|
|
case let arg as FunctionArgument:
|
|
if let res = result, res != self.result {
|
|
self.result = nil
|
|
return .abortWalk
|
|
}
|
|
self.result = .pointer(arg)
|
|
return .continueWalk
|
|
case let atp as AddressToPointerInst:
|
|
if case let .address(result) = result, atp.operand != result {
|
|
self.result = nil
|
|
return .abortWalk
|
|
}
|
|
|
|
if addrType == atp.operand.type, path.isEmpty {
|
|
self.result = .address(atp.operand)
|
|
return .continueWalk
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
self.result = nil
|
|
return .abortWalk
|
|
}
|
|
|
|
mutating func walkUp(value: Value, path: SmallProjectionPath) -> WalkResult {
|
|
switch value {
|
|
case is BlockArgument, is MarkDependenceInst, is CopyValueInst,
|
|
is StructExtractInst, is TupleExtractInst, is StructInst, is TupleInst,
|
|
is FunctionArgument, is AddressToPointerInst:
|
|
return walkUpDefault(value: value, path: path)
|
|
default:
|
|
self.result = nil
|
|
return .abortWalk
|
|
}
|
|
}
|
|
|
|
var walkUpCache = WalkerCache<Path>()
|
|
}
|
|
}
|
|
|
|
/// The `EnclosingScope` of an access is the innermost `begin_access`
|
|
/// instruction that checks for exclusivity of the access.
|
|
/// If there is no `begin_access` instruction found, then the scope is
|
|
/// the base itself.
|
|
///
|
|
/// The access scopes for the snippet below are:
|
|
/// (l1, .base(%addr)), (l2, .scope(%a2)), (l3, .scope(%a3))
|
|
///
|
|
/// ````
|
|
/// %addr = ... : $*Int64
|
|
/// %l1 = load %addr : $*Int64
|
|
/// %a1 = begin_access [read] [dynamic] %addr : $*Int64
|
|
/// %a2 = begin_access [read] [dynamic] %addr : $*Int64
|
|
/// %l2 = load %a2 : $*Int64
|
|
/// end_access %a2 : $*Int64
|
|
/// end_access %a1 : $*Int64
|
|
/// %a3 = begin_access [read] [dynamic] [no_nested_conflict] %addr : $*Int64
|
|
/// %l3 = load %a3 : $*Int64
|
|
/// end_access %a3 : $*Int64
|
|
/// ```
|
|
enum EnclosingScope {
|
|
case scope(BeginAccessInst)
|
|
case base(AccessBase)
|
|
}
|
|
|
|
/// A walker utility that, given an address value, computes the `AccessPath`
|
|
/// of the access (the base address and the address projections to the accessed fields) and
|
|
/// the innermost enclosing scope (`begin_access`).
|
|
struct AccessPathWalker {
|
|
mutating func getAccessPath(of address: Value) -> AccessPath? {
|
|
assert(address.type.isAddress, "Expected address")
|
|
walker.start()
|
|
if walker.walkUp(address: address, path: Walker.Path()) == .abortWalk {
|
|
return nil
|
|
}
|
|
return walker.result
|
|
}
|
|
|
|
mutating func getAccessPathWithScope(of address: Value) -> (AccessPath?, EnclosingScope?) {
|
|
let ap = getAccessPath(of: address)
|
|
return (ap, walker.scope)
|
|
}
|
|
|
|
mutating func getAccessBase(of address: Value) -> AccessBase? {
|
|
getAccessPath(of: address)?.base
|
|
}
|
|
|
|
mutating func getAccessScope(of address: Value) -> EnclosingScope? {
|
|
getAccessPathWithScope(of: address).1
|
|
}
|
|
|
|
private var walker = Walker()
|
|
|
|
private struct Walker : AddressUseDefWalker {
|
|
private(set) var result: AccessPath? = nil
|
|
private var foundBeginAccess: BeginAccessInst? = nil
|
|
private var pointerId = PointerIdentification()
|
|
|
|
var scope: EnclosingScope? {
|
|
if let ba = foundBeginAccess {
|
|
return .scope(ba)
|
|
} else {
|
|
guard let accessPath = result else { return nil }
|
|
return .base(accessPath.base)
|
|
}
|
|
}
|
|
|
|
mutating func start() {
|
|
result = nil
|
|
foundBeginAccess = nil
|
|
}
|
|
|
|
struct Path : SmallProjectionWalkingPath {
|
|
let projectionPath: SmallProjectionPath
|
|
|
|
// Tracks whether an `index_addr` instruction was crossed.
|
|
// It should be (FIXME: check if it's enforced) that operands
|
|
// of `index_addr` must be `tail_addr` or other `index_addr` results.
|
|
let indexAddr: Bool
|
|
|
|
init(projectionPath: SmallProjectionPath = SmallProjectionPath(), indexAddr: Bool = false) {
|
|
self.projectionPath = projectionPath
|
|
self.indexAddr = indexAddr
|
|
}
|
|
|
|
func with(projectionPath: SmallProjectionPath) -> Self {
|
|
return Self(projectionPath: projectionPath, indexAddr: indexAddr)
|
|
}
|
|
|
|
func with(indexAddr: Bool) -> Self {
|
|
return Self(projectionPath: projectionPath, indexAddr: indexAddr)
|
|
}
|
|
|
|
func merge(with other: Self) -> Self {
|
|
return Self(
|
|
projectionPath: projectionPath.merge(with: other.projectionPath),
|
|
indexAddr: indexAddr || other.indexAddr
|
|
)
|
|
}
|
|
}
|
|
|
|
mutating func rootDef(address: Value, path: Path) -> WalkResult {
|
|
// Try identifying the address a pointer originates from
|
|
if let p2ai = address as? PointerToAddressInst {
|
|
if let addrOrArg = pointerId.getOriginatingAddressOrArgument(p2ai) {
|
|
switch addrOrArg {
|
|
case let .address(newAddress):
|
|
return walkUp(address: newAddress, path: path)
|
|
case let .pointer(arg):
|
|
self.result = AccessPath(base: AccessBase(baseAddress: arg, kind: .pointer), projectionPath: path.projectionPath)
|
|
}
|
|
} else {
|
|
self.result = AccessPath(base: AccessBase(baseAddress: p2ai, kind: .pointer), projectionPath: path.projectionPath)
|
|
return .continueWalk
|
|
}
|
|
}
|
|
|
|
// If this is a base then we're done
|
|
if let base = AccessBase(baseAddress: address) {
|
|
self.result = AccessPath(base: base, projectionPath: path.projectionPath)
|
|
return .continueWalk
|
|
}
|
|
|
|
// The base is unidentified
|
|
self.result = nil
|
|
return .abortWalk
|
|
}
|
|
|
|
mutating func walkUp(address: Value, path: Path) -> WalkResult {
|
|
if address is IndexAddrInst {
|
|
// Track that we crossed an `index_addr` during the walk-up
|
|
return walkUpDefault(address: address, path: path.with(indexAddr: true))
|
|
} else if path.indexAddr && !canBeOperandOfIndexAddr(address) {
|
|
// An `index_addr` instruction cannot be derived from an address
|
|
// projection. Bail out
|
|
self.result = nil
|
|
return .abortWalk
|
|
} else if let ba = address as? BeginAccessInst, foundBeginAccess == nil {
|
|
foundBeginAccess = ba
|
|
}
|
|
return walkUpDefault(address: address, path: path.with(indexAddr: false))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A ValueUseDef walker that identifies which values a reference might
|
|
/// originate from.
|
|
protocol AccessStoragePathWalker : ValueUseDefWalker where Path == SmallProjectionPath {
|
|
mutating func visit(access: AccessStoragePath)
|
|
}
|
|
|
|
extension AccessStoragePathWalker {
|
|
// Main entry point
|
|
/// Given an `ap: AccessPath` where the base address is a projection
|
|
/// from a reference type, returns the set of reference definitions
|
|
/// that the address of the access may originate from.
|
|
mutating func getAccessStorage(for accessPath: AccessPath) {
|
|
walkUpCache.clear()
|
|
let path = accessPath.projectionPath
|
|
switch accessPath.base.baseAddress {
|
|
case let rea as RefElementAddrInst:
|
|
_ = walkUp(value: rea.operand, path: path.push(.classField, index: rea.fieldIndex))
|
|
case let pb as ProjectBoxInst:
|
|
_ = walkUp(value: pb.operand, path: path.push(.classField, index: pb.fieldIndex))
|
|
case let rta as RefTailAddrInst:
|
|
_ = walkUp(value: rta.operand, path: path.push(.tailElements, index: 0))
|
|
default:
|
|
visit(access: AccessStoragePath(storage: accessPath.base.baseAddress, path: accessPath.projectionPath))
|
|
}
|
|
}
|
|
|
|
mutating func rootDef(value: Value, path: SmallProjectionPath) -> WalkResult {
|
|
visit(access: AccessStoragePath(storage: value, path: path))
|
|
return .continueWalk
|
|
}
|
|
}
|