Skip to content

New escape analysis, implemented in libswift #39438

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

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions SwiftCompilerSources/Sources/Optimizer/Analysis/AliasAnalysis.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,108 @@ struct AliasAnalysis {
// Any other non-address value means: all addresses of any referenced class instances within the value.
return EscapeInfo.Path(.anyValueFields).push(.anyClassField).push(.anyValueFields)
}

static func register() {
AliasAnalysis_register(
// initFn
{ (buffer: UnsafeMutableRawPointer, size: Int, ca: BridgedCalleeAnalysis) in
precondition(MemoryLayout<EscapeInfo>.size <= size, "wrong EscapeInfo size")
let ei = EscapeInfo(calleeAnalysis: CalleeAnalysis(bridged: ca))
buffer.initializeMemory(as: EscapeInfo.self, repeating: ei, count: 1)
},
// destroyFn
{ (buffer: UnsafeMutableRawPointer, size: Int) in
precondition(MemoryLayout<EscapeInfo>.size <= size, "wrong EscapeInfo size")
buffer.assumingMemoryBound(to: EscapeInfo.self).deinitialize(count: 1)
},
// isAddrEscaping2InstFn
{ (buffer: UnsafeMutableRawPointer, bridgedVal: BridgedValue, bridgedInst: BridgedInstruction,
readingOpsMask: UInt64, writingOpsMask: UInt64) -> BridgedMemoryBehavior in
let eiPointer = buffer.assumingMemoryBound(to: EscapeInfo.self)
let inst = bridgedInst.instruction
let val = bridgedVal.value
let path = AliasAnalysis.getPtrOrAddressPath(for: val)
var isReading = false
var isWriting = false
if eiPointer.pointee.isEscaping(addressesOf: val, path: path,
visitUse: { op, _, _ in
let user = op.instruction
if user == inst {
let opIdx = op.index
if opIdx >= 64 || (readingOpsMask & (UInt64(1) << opIdx)) != 0 {
isReading = true
}
if opIdx >= 64 || (writingOpsMask & (UInt64(1) << opIdx)) != 0 {
isWriting = true
}
}
if user is ReturnInst {
// Anything which is returned cannot escape to an instruction inside the function.
return .ignore
}
return .continueWalking
}) {
return MayReadWriteBehavior
}
switch (isReading, isWriting) {
case (false, false): return NoneBehavior
case (true, false): return MayReadBehavior
case (false, true): return MayWriteBehavior
case (true, true): return MayReadWriteBehavior
}
},
// isObjEscaping2InstFn
{ (buffer: UnsafeMutableRawPointer, bridgedObj: BridgedValue, bridgedInst: BridgedInstruction,
operandMask: UInt64) -> Int in
let eiPointer = buffer.assumingMemoryBound(to: EscapeInfo.self)
let inst = bridgedInst.instruction
let obj = bridgedObj.value
let path = EscapeInfo.Path(.anyValueFields)
return eiPointer.pointee.isEscaping(object: obj, path: path,
visitUse: { op, _, _ in
let user = op.instruction
if user == inst {
let opIdx = op.index
if opIdx >= 64 || (operandMask & (UInt64(1) << opIdx)) != 0 {
return .markEscaping
}
}
if user is ReturnInst {
// Anything which is returned cannot escape to an instruction inside the function.
return .ignore
}
return .continueWalking
}) ? 1 : 0
},
// isAddrVisibleFromObj
{ (buffer: UnsafeMutableRawPointer, bridgedAddr: BridgedValue, bridgedObj: BridgedValue) -> Int in
let eiPointer = buffer.assumingMemoryBound(to: EscapeInfo.self)
let addr = bridgedAddr.value
let obj = bridgedObj.value

// Is the `addr` within all reachable objects/addresses, when start walking from `obj`?
// Try both directions: 1. from addr -> obj
return eiPointer.pointee.isEscaping(addressesOf: addr, path: EscapeInfo.Path(.anyValueFields),
visitUse: { op, _, _ in
if op.value == obj {
return .markEscaping
}
if op.instruction is ReturnInst {
// Anything which is returned cannot escape to an instruction inside the function.
return .ignore
}
return .continueWalking
}) ? 1 : 0
},
// canReferenceSameFieldFn
{ (buffer: UnsafeMutableRawPointer, bridgedLhs: BridgedValue, bridgedRhs: BridgedValue) -> Int in
let eiPointer = buffer.assumingMemoryBound(to: EscapeInfo.self)
let lhs = bridgedLhs.value
let rhs = bridgedRhs.value
return eiPointer.pointee.canReferenceSameField(
lhs, path: AliasAnalysis.getPtrOrAddressPath(for: lhs),
rhs, path: AliasAnalysis.getPtrOrAddressPath(for: rhs)) ? 1 : 0
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ swift_compiler_sources(Optimizer
RangeDumper.swift
ReleaseDevirtualizer.swift
RunUnitTests.swift
StackPromotion.swift
)
Original file line number Diff line number Diff line change
Expand Up @@ -71,20 +71,32 @@ private func tryDevirtualizeReleaseOfObject(
_ deallocStackRef: DeallocStackRefInst
) {
let allocRefInstruction = deallocStackRef.allocRef
var root = release.operands[0].value
while let newRoot = stripRCIdentityPreservingInsts(root) {
root = newRoot
let type = allocRefInstruction.type
let calleeAnalysis = context.calleeAnalysis

guard let dealloc = calleeAnalysis.getDestructor(ofExactType: type) else {
return
}

if root != allocRefInstruction {
guard let uniqueReference = getUniqueReference(of: release.operand) else {
return
}

let type = allocRefInstruction.type

guard let dealloc = context.calleeAnalysis.getDestructor(ofExactType: type) else {
var escapeInfo = EscapeInfo(calleeAnalysis: calleeAnalysis)
var found = false
if escapeInfo.isEscaping(object: uniqueReference,
visitUse: { op, path, _ in
if op.value === allocRefInstruction {
if !path.isEmpty { return .markEscaping }
found = true
return .ignore
}
if op.value is Allocation { return .markEscaping }
return .continueWalking
}) {
return
}
assert(found, "value must come from an allocation")

let builder = Builder(at: release, location: release.location, context)

Expand All @@ -106,119 +118,31 @@ private func tryDevirtualizeReleaseOfObject(
context.erase(instruction: release)
}

private func stripRCIdentityPreservingInsts(_ value: Value) -> Value? {
guard let inst = value as? Instruction else { return nil }

switch inst {
// First strip off RC identity preserving casts.
case is UpcastInst,
is UncheckedRefCastInst,
is InitExistentialRefInst,
is OpenExistentialRefInst,
is RefToBridgeObjectInst,
is BridgeObjectToRefInst,
is ConvertFunctionInst,
is UncheckedEnumDataInst:
return inst.operands[0].value

// Then if we have a struct_extract that is extracting a non-trivial member
// from a struct with no other non-trivial members, a ref count operation on
// the struct is equivalent to a ref count operation on the extracted
// member. Strip off the extract.
case let sei as StructExtractInst where sei.isFieldOnlyNonTrivialField:
return sei.operand

// If we have a struct or tuple instruction with only one non-trivial operand, the
// only reference count that can be modified is the non-trivial operand. Return
// the non-trivial operand.
case is StructInst, is TupleInst:
return inst.uniqueNonTrivialOperand

// If we have an enum instruction with a payload, strip off the enum to
// expose the enum's payload.
case let ei as EnumInst where !ei.operands.isEmpty:
return ei.operand

// If we have a tuple_extract that is extracting the only non trivial member
// of a tuple, a retain_value on the tuple is equivalent to a retain_value on
// the extracted value.
case let tei as TupleExtractInst where tei.isEltOnlyNonTrivialElt:
return tei.operand

default:
return nil
}
}

private extension Instruction {
/// Search the operands of this tuple for a unique non-trivial elt. If we find
/// it, return it. Otherwise return `nil`.
var uniqueNonTrivialOperand: Value? {
var candidateElt: Value?
let function = self.function

for op in operands {
if !op.value.type.isTrivial(in: function) {
if candidateElt == nil {
candidateElt = op.value
continue
private func getUniqueReference(of value: Value) -> Value? {
let function = value.function
var val = value
while true {
if val.type.isClass {
return val
}
switch val {
case let ei as EnumInst where !ei.operands.isEmpty:
val = ei.operand
case is StructInst, is TupleInst:
var uniqueNonTrivialOperand: Value?

for op in (val as! SingleValueInstruction).operands {
if !op.value.type.isTrivial(in: function) {
if let _ = uniqueNonTrivialOperand {
return nil
}
uniqueNonTrivialOperand = op.value
}
}

// Otherwise, we have two values that are non-trivial. Bail.
guard let uniqueOp = uniqueNonTrivialOperand else { return nil }
val = uniqueOp
default:
return nil
}
}

return candidateElt
}
}

private extension TupleExtractInst {
var isEltOnlyNonTrivialElt: Bool {
let function = self.function

if type.isTrivial(in: function) {
return false
}

let opType = operand.type

var nonTrivialEltsCount = 0
for elt in opType.tupleElements {
if elt.isTrivial(in: function) {
nonTrivialEltsCount += 1
}

if nonTrivialEltsCount > 1 {
return false
}
}

return true
}
}

private extension StructExtractInst {
var isFieldOnlyNonTrivialField: Bool {
let function = self.function

if type.isTrivial(in: function) {
return false
}

let structType = operand.type

var nonTrivialFieldsCount = 0
for field in structType.getNominalFields(in: function) {
if field.isTrivial(in: function) {
nonTrivialFieldsCount += 1
}

if nonTrivialFieldsCount > 1 {
return false
}
}

return true
}
}
Loading