transform: follow aggregate returned alloc aliases

Track whether an allocation reaches a callee return separately from the
instruction where it escapes. The extra result state is needed because LLVM's
returned parameter attribute only covers scalar returns: a slice helper returns
its data pointer inside a {ptr, len, cap} aggregate, so the alias is only
visible by walking insertvalue and ret uses in the callee.

This lets OptimizeAllocs keep the backing array for returnIntSlice(s) on the
stack when the returned slice is ignored, matching gc's escape decision for the
same pattern. The golden output still keeps the escaping returned-slice case on
the heap.
This commit is contained in:
Jake Bailey
2026-06-12 11:22:20 -07:00
committed by Ron Evans
parent a683881eb7
commit 27f8ec17fc
3 changed files with 57 additions and 28 deletions
+56 -25
View File
@@ -162,21 +162,41 @@ func FormatAllocCover(pos token.Position) string {
// valueEscapesAt returns the instruction where the given value may escape and a // valueEscapesAt returns the instruction where the given value may escape and a
// nil llvm.Value if it definitely doesn't. The value must be an instruction. // nil llvm.Value if it definitely doesn't. The value must be an instruction.
func valueEscapesAt(value llvm.Value) llvm.Value { func valueEscapesAt(value llvm.Value) llvm.Value {
return valueEscapesAtImpl(value, false, nil) return valueEscapesAtImpl(value, false, nil).escapeAt
} }
func valueEscapesAtImpl(value llvm.Value, allowReturn bool, visiting map[llvm.Value]struct{}) llvm.Value { type escapeResult struct {
escapeAt llvm.Value
// returned is separate from escapeAt because values can flow to a return
// through aggregate operations. LLVM can mark a scalar returned parameter,
// but a slice data pointer returned inside {ptr, len, cap} is only visible
// after walking insertvalue/ret uses in the callee.
returned bool
}
func (r *escapeResult) merge(other escapeResult) bool {
if !other.escapeAt.IsNil() {
r.escapeAt = other.escapeAt
return false
}
r.returned = r.returned || other.returned
return true
}
func valueEscapesAtImpl(value llvm.Value, allowReturn bool, visiting map[llvm.Value]struct{}) escapeResult {
if visiting == nil { if visiting == nil {
visiting = make(map[llvm.Value]struct{}) visiting = make(map[llvm.Value]struct{})
} }
if _, ok := visiting[value]; ok { if _, ok := visiting[value]; ok {
// Recursive call graph while following returned parameters. Treat this // Recursive call graph while following returned parameters. Treat this
// as escaping to keep the analysis conservative and bounded. // as escaping to keep the analysis conservative and bounded.
return value return escapeResult{escapeAt: value}
} }
visiting[value] = struct{}{} visiting[value] = struct{}{}
defer delete(visiting, value) defer delete(visiting, value)
var result escapeResult
uses := getUses(value) uses := getUses(value)
for _, use := range uses { for _, use := range uses {
if use.IsAInstruction().IsNil() { if use.IsAInstruction().IsNil() {
@@ -184,13 +204,23 @@ func valueEscapesAtImpl(value llvm.Value, allowReturn bool, visiting map[llvm.Va
} }
switch use.InstructionOpcode() { switch use.InstructionOpcode() {
case llvm.GetElementPtr: case llvm.GetElementPtr:
if at := valueEscapesAtImpl(use, allowReturn, visiting); !at.IsNil() { if !result.merge(valueEscapesAtImpl(use, allowReturn, visiting)) {
return at return result
} }
case llvm.BitCast: case llvm.BitCast:
// A bitcast escapes if the casted-to value escapes. // A bitcast escapes if the casted-to value escapes.
if at := valueEscapesAtImpl(use, allowReturn, visiting); !at.IsNil() { if !result.merge(valueEscapesAtImpl(use, allowReturn, visiting)) {
return at return result
}
case llvm.InsertValue:
if !result.merge(valueEscapesAtImpl(use, allowReturn, visiting)) {
return result
}
case llvm.ExtractValue:
if use.Type().TypeKind() == llvm.PointerTypeKind {
if !result.merge(valueEscapesAtImpl(use, allowReturn, visiting)) {
return result
}
} }
case llvm.Load: case llvm.Load:
// Load does not escape. // Load does not escape.
@@ -198,41 +228,42 @@ func valueEscapesAtImpl(value llvm.Value, allowReturn bool, visiting map[llvm.Va
// Store only escapes when the value is stored to, not when the // Store only escapes when the value is stored to, not when the
// value is stored into another value. // value is stored into another value.
if use.Operand(0) == value { if use.Operand(0) == value {
return use return escapeResult{escapeAt: use}
} }
case llvm.Call: case llvm.Call:
if at := callValueEscapesAt(use, value, allowReturn, visiting); !at.IsNil() { if !result.merge(callValueEscapesAt(use, value, allowReturn, visiting)) {
return at return result
} }
case llvm.ICmp: case llvm.ICmp:
// Comparing pointers don't let the pointer escape. // Comparing pointers don't let the pointer escape.
// This is often a compiler-inserted nil check. // This is often a compiler-inserted nil check.
case llvm.Ret: case llvm.Ret:
if !allowReturn || use.Operand(0) != value { if !allowReturn || use.Operand(0) != value {
return use return escapeResult{escapeAt: use}
} }
result.returned = true
default: default:
// Unknown instruction, might escape. // Unknown instruction, might escape.
return use return escapeResult{escapeAt: use}
} }
} }
// Checked all uses, and none let the pointer value escape. // Checked all uses, and none let the pointer value escape.
return llvm.Value{} return result
} }
// callValueEscapesAt returns whether value escapes through this call. It also // callValueEscapesAt returns whether value escapes through this call. It also
// handles calls that return value unchanged, as long as the called function does // handles calls that return value unchanged, as long as the called function does
// not otherwise capture the parameter and the returned alias does not escape. // not otherwise capture the parameter and the returned alias does not escape.
func callValueEscapesAt(call, value llvm.Value, allowReturn bool, visiting map[llvm.Value]struct{}) llvm.Value { func callValueEscapesAt(call, value llvm.Value, allowReturn bool, visiting map[llvm.Value]struct{}) escapeResult {
called := call.CalledValue() called := call.CalledValue()
if called.IsAFunction().IsNil() { if called.IsAFunction().IsNil() {
return call return escapeResult{escapeAt: call}
} }
kindNoCapture := llvm.AttributeKindID("nocapture") kindNoCapture := llvm.AttributeKindID("nocapture")
kindReturned := llvm.AttributeKindID("returned") kindReturned := llvm.AttributeKindID("returned")
matched := false matched := false
returned := false var result escapeResult
for i := 0; i < called.ParamsCount(); i++ { for i := 0; i < called.ParamsCount(); i++ {
if call.Operand(i) != value { if call.Operand(i) != value {
continue continue
@@ -242,30 +273,30 @@ func callValueEscapesAt(call, value llvm.Value, allowReturn bool, visiting map[l
nocapture := !called.GetEnumAttributeAtIndex(index, kindNoCapture).IsNil() nocapture := !called.GetEnumAttributeAtIndex(index, kindNoCapture).IsNil()
returnedParam := !called.GetEnumAttributeAtIndex(index, kindReturned).IsNil() returnedParam := !called.GetEnumAttributeAtIndex(index, kindReturned).IsNil()
if returnedParam { if returnedParam {
returned = true result.returned = true
} }
if nocapture { if nocapture {
continue continue
} }
if !returnedParam || called.IsDeclaration() { if called.IsDeclaration() {
return call return escapeResult{escapeAt: call}
} }
if at := valueEscapesAtImpl(called.Param(i), true, visiting); !at.IsNil() { if !result.merge(valueEscapesAtImpl(called.Param(i), true, visiting)) {
return at return result
} }
} }
for i := called.ParamsCount(); i < call.OperandsCount(); i++ { for i := called.ParamsCount(); i < call.OperandsCount(); i++ {
if call.Operand(i) == value { if call.Operand(i) == value {
return call return escapeResult{escapeAt: call}
} }
} }
if !matched { if !matched {
return llvm.Value{} return escapeResult{}
} }
if returned { if result.returned {
return valueEscapesAtImpl(call, allowReturn, visiting) return valueEscapesAtImpl(call, allowReturn, visiting)
} }
return llvm.Value{} return escapeResult{}
} }
func lineLengthAt(filename string, lineNumber int) int { func lineLengthAt(filename string, lineNumber int) int {
-1
View File
@@ -1,4 +1,3 @@
testdata/allocs2.go:21.1,21.22 1 0
testdata/allocs2.go:24.1,24.43 1 0 testdata/allocs2.go:24.1,24.43 1 0
testdata/allocs2.go:26.1,26.25 1 0 testdata/allocs2.go:26.1,26.25 1 0
testdata/allocs2.go:29.1,29.22 1 0 testdata/allocs2.go:29.1,29.22 1 0
+1 -2
View File
@@ -1,4 +1,3 @@
testdata/allocs2.go:21:12: object allocated on the heap: escapes at line 22
testdata/allocs2.go:24:15: object allocated on the heap: size is not constant testdata/allocs2.go:24:15: object allocated on the heap: size is not constant
testdata/allocs2.go:26:12: object allocated on the heap: object size 300 exceeds maximum stack allocation size 256 testdata/allocs2.go:26:12: object allocated on the heap: object size 300 exceeds maximum stack allocation size 256
testdata/allocs2.go:29:12: object allocated on the heap: escapes at line 30 testdata/allocs2.go:29:12: object allocated on the heap: escapes at line 30
@@ -9,4 +8,4 @@ testdata/allocs2.go:49:2: object allocated on the heap: escapes at line 51
testdata/allocs2.go:50:2: object allocated on the heap: escapes at line 51 testdata/allocs2.go:50:2: object allocated on the heap: escapes at line 51
testdata/allocs2.go:107:11: object allocated on the heap: escapes at line 108 testdata/allocs2.go:107:11: object allocated on the heap: escapes at line 108
testdata/allocs2.go:114:2: object allocated on the heap: escapes at line 117 testdata/allocs2.go:114:2: object allocated on the heap: escapes at line 117
testdata/allocs2.go:128:2: object allocated on the heap: escapes at line 130 testdata/allocs2.go:128:2: object allocated on the heap: escapes at unknown line