Skip to content

Commit

Permalink
cmd/compile/internal/inline: analyze function result properties
Browse files Browse the repository at this point in the history
Add code to analyze properties of function result values, specifically
heuristics for cases where we always return allocated memory, always
return the same constant, or always return the same function.

Updates #61502.

Change-Id: I8b0a3295b5be7f7ad4c2d5b9803925aea0639376
Reviewed-on: https://go-review.googlesource.com/c/go/+/511559
Reviewed-by: Matthew Dempsky <mdempsky@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
  • Loading branch information
thanm committed Sep 6, 2023
1 parent 3d62c76 commit 3f0f767
Show file tree
Hide file tree
Showing 8 changed files with 659 additions and 30 deletions.
7 changes: 5 additions & 2 deletions src/cmd/compile/internal/inline/inl.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ func InlinePackage(p *pgo.Profile) {
garbageCollectUnreferencedHiddenClosures()

if base.Debug.DumpInlFuncProps != "" {
inlheur.DumpFuncProps(nil, base.Debug.DumpInlFuncProps)
inlheur.DumpFuncProps(nil, base.Debug.DumpInlFuncProps, nil)
}
}

Expand Down Expand Up @@ -293,7 +293,10 @@ func CanInline(fn *ir.Func, profile *pgo.Profile) {
}

if base.Debug.DumpInlFuncProps != "" {
defer inlheur.DumpFuncProps(fn, base.Debug.DumpInlFuncProps)
inlheur.DumpFuncProps(fn, base.Debug.DumpInlFuncProps,
func(fn *ir.Func) {
CanInline(fn, profile)
})
}

var reason string // reason, if any, that the function was not inlined
Expand Down
20 changes: 14 additions & 6 deletions src/cmd/compile/internal/inline/inlheur/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
const (
debugTraceFuncs = 1 << iota
debugTraceFuncFlags
debugTraceResults
)

// propAnalyzer interface is used for defining one or more analyzer
Expand Down Expand Up @@ -48,18 +49,21 @@ type fnInlHeur struct {
// computeFuncProps examines the Go function 'fn' and computes for it
// a function "properties" object, to be used to drive inlining
// heuristics. See comments on the FuncProps type for more info.
func computeFuncProps(fn *ir.Func) *FuncProps {
func computeFuncProps(fn *ir.Func, canInline func(*ir.Func)) *FuncProps {
enableDebugTraceIfEnv()
if debugTrace&debugTraceFuncs != 0 {
fmt.Fprintf(os.Stderr, "=-= starting analysis of func %v:\n%+v\n",
fn.Sym().Name, fn)
}
ra := makeResultsAnalyzer(fn, canInline)
ffa := makeFuncFlagsAnalyzer(fn)
analyzers := []propAnalyzer{ffa}
analyzers := []propAnalyzer{ffa, ra}
fp := new(FuncProps)
runAnalyzersOnFunction(fn, analyzers)
for _, a := range analyzers {
a.setResults(fp)
}
disableDebugTrace()
return fp
}

Expand All @@ -83,13 +87,17 @@ func fnFileLine(fn *ir.Func) (string, uint) {
return filepath.Base(p.Filename()), p.Line()
}

func UnitTesting() bool {
return base.Debug.DumpInlFuncProps != ""
}

// DumpFuncProps computes and caches function properties for the func
// 'fn', or if fn is nil, writes out the cached set of properties to
// the file given in 'dumpfile'. Used for the "-d=dumpinlfuncprops=..."
// command line flag, intended for use primarily in unit testing.
func DumpFuncProps(fn *ir.Func, dumpfile string) {
func DumpFuncProps(fn *ir.Func, dumpfile string, canInline func(*ir.Func)) {
if fn != nil {
captureFuncDumpEntry(fn)
captureFuncDumpEntry(fn, canInline)
} else {
emitDumpToFile(dumpfile)
}
Expand Down Expand Up @@ -132,7 +140,7 @@ func emitDumpToFile(dumpfile string) {

// captureFuncDumpEntry analyzes function 'fn' and adds a entry
// for it to 'dumpBuffer'. Used for unit testing.
func captureFuncDumpEntry(fn *ir.Func) {
func captureFuncDumpEntry(fn *ir.Func, canInline func(*ir.Func)) {
// avoid capturing compiler-generated equality funcs.
if strings.HasPrefix(fn.Sym().Name, ".eq.") {
return
Expand All @@ -145,7 +153,7 @@ func captureFuncDumpEntry(fn *ir.Func) {
// so don't add them more than once.
return
}
fp := computeFuncProps(fn)
fp := computeFuncProps(fn, canInline)
file, line := fnFileLine(fn)
entry := fnInlHeur{
fname: fn.Sym().Name,
Expand Down
260 changes: 260 additions & 0 deletions src/cmd/compile/internal/inline/inlheur/analyze_func_returns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package inlheur

import (
"cmd/compile/internal/ir"
"fmt"
"go/constant"
"go/token"
"os"
)

// returnsAnalyzer stores state information for the process of
// computing flags/properties for the return values of a specific Go
// function, as part of inline heuristics synthesis.
type returnsAnalyzer struct {
fname string
props []ResultPropBits
values []resultVal
canInline func(*ir.Func)
}

// resultVal captures information about a specific result returned from
// the function we're analyzing; we are interested in cases where
// the func always returns the same constant, or always returns
// the same function, etc. This container stores info on a the specific
// scenarios we're looking for.
type resultVal struct {
lit constant.Value
fn *ir.Name
fnClo bool
top bool
}

func makeResultsAnalyzer(fn *ir.Func, canInline func(*ir.Func)) *returnsAnalyzer {
results := fn.Type().Results()
props := make([]ResultPropBits, len(results))
vals := make([]resultVal, len(results))
for i := range results {
rt := results[i].Type
if !rt.IsScalar() && !rt.HasNil() {
// existing properties not applicable here (for things
// like structs, arrays, slices, etc).
props[i] = ResultNoInfo
continue
}
// set the "top" flag (as in "top element of data flow lattice")
// meaning "we have no info yet, but we might later on".
vals[i].top = true
}
return &returnsAnalyzer{
props: props,
values: vals,
canInline: canInline,
}
}

// setResults transfers the calculated result properties for this
// function to 'fp'.
func (ra *returnsAnalyzer) setResults(fp *FuncProps) {
// Promote ResultAlwaysSameFunc to ResultAlwaysSameInlinableFunc
for i := range ra.values {
if ra.props[i] == ResultAlwaysSameFunc {
f := ra.values[i].fn.Func
// If the function being returns is a closure that hasn't
// yet been checked by CanInline, invoke it now. NB: this
// is hacky, it would be better if things were structured
// so that all closures were visited ahead of time.
if ra.values[i].fnClo {
if f != nil && !f.InlinabilityChecked() {
ra.canInline(f)
}
}
if f.Inl != nil {
ra.props[i] = ResultAlwaysSameInlinableFunc
}
}
}
fp.ResultFlags = ra.props
}

func (ra *returnsAnalyzer) pessimize() {
for i := range ra.props {
ra.props[i] = ResultNoInfo
}
}

func (ra *returnsAnalyzer) nodeVisitPre(n ir.Node) {
}

func (ra *returnsAnalyzer) nodeVisitPost(n ir.Node) {
if len(ra.values) == 0 {
return
}
if n.Op() != ir.ORETURN {
return
}
if debugTrace&debugTraceResults != 0 {
fmt.Fprintf(os.Stderr, "=+= returns nodevis %v %s\n",
ir.Line(n), n.Op().String())
}

// No support currently for named results, so if we see an empty
// "return" stmt, be conservative.
rs := n.(*ir.ReturnStmt)
if len(rs.Results) != len(ra.values) {
ra.pessimize()
return
}
for i, r := range rs.Results {
ra.analyzeResult(i, r)
}
}

// isFuncName returns the *ir.Name for the func or method
// corresponding to node 'n', along with a boolean indicating success,
// and another boolean indicating whether the func is closure.
func isFuncName(n ir.Node) (*ir.Name, bool, bool) {
sv := ir.StaticValue(n)
if sv.Op() == ir.ONAME {
name := sv.(*ir.Name)
if name.Sym() != nil && name.Class == ir.PFUNC {
return name, true, false
}
}
if sv.Op() == ir.OCLOSURE {
cloex := sv.(*ir.ClosureExpr)
return cloex.Func.Nname, true, true
}
if sv.Op() == ir.OMETHEXPR {
if mn := ir.MethodExprName(sv); mn != nil {
return mn, true, false
}
}
return nil, false, false
}

// analyzeResult examines the expression 'n' being returned as the
// 'ii'th argument in some return statement to see whether has
// interesting characteristics (for example, returns a constant), then
// applies a dataflow "meet" operation to combine this result with any
// previous result (for the given return slot) that we've already
// processed.
func (ra *returnsAnalyzer) analyzeResult(ii int, n ir.Node) {
isAllocMem := isAllocatedMem(n)
isConcConvItf := isConcreteConvIface(n)
lit, isConst := isLiteral(n)
rfunc, isFunc, isClo := isFuncName(n)
curp := ra.props[ii]
newp := ResultNoInfo
var newlit constant.Value
var newfunc *ir.Name

if debugTrace&debugTraceResults != 0 {
fmt.Fprintf(os.Stderr, "=-= %v: analyzeResult n=%s ismem=%v isconcconv=%v isconst=%v isfunc=%v isclo=%v\n", ir.Line(n), n.Op().String(), isAllocMem, isConcConvItf, isConst, isFunc, isClo)
}

if ra.values[ii].top {
ra.values[ii].top = false
// this is the first return we've seen; record
// whatever properties it has.
switch {
case isAllocMem:
newp = ResultIsAllocatedMem
case isConcConvItf:
newp = ResultIsConcreteTypeConvertedToInterface
case isFunc:
newp = ResultAlwaysSameFunc
newfunc = rfunc
case isConst:
newp = ResultAlwaysSameConstant
newlit = lit
}
} else {
// this is not the first return we've seen; apply
// what amounts of a "meet" operator to combine
// the properties we see here with what we saw on
// the previous returns.
switch curp {
case ResultIsAllocatedMem:
if isAllocatedMem(n) {
newp = ResultIsAllocatedMem
}
case ResultIsConcreteTypeConvertedToInterface:
if isConcreteConvIface(n) {
newp = ResultIsConcreteTypeConvertedToInterface
}
case ResultAlwaysSameConstant:
if isConst && isSameLiteral(lit, ra.values[ii].lit) {
newp = ResultAlwaysSameConstant
newlit = lit
}
case ResultAlwaysSameFunc:
if isFunc && isSameFuncName(rfunc, ra.values[ii].fn) {
newp = ResultAlwaysSameFunc
newfunc = rfunc
}
}
}
ra.values[ii].fn = newfunc
ra.values[ii].fnClo = isClo
ra.values[ii].lit = newlit
ra.props[ii] = newp

if debugTrace&debugTraceResults != 0 {
fmt.Fprintf(os.Stderr, "=-= %v: analyzeResult newp=%s\n",
ir.Line(n), newp)
}

}

func isAllocatedMem(n ir.Node) bool {
sv := ir.StaticValue(n)
switch sv.Op() {
case ir.OMAKESLICE, ir.ONEW, ir.OPTRLIT, ir.OSLICELIT:
return true
}
return false
}

func isLiteral(n ir.Node) (constant.Value, bool) {
sv := ir.StaticValue(n)
if sv.Op() == ir.ONIL {
return nil, true
}
if sv.Op() != ir.OLITERAL {
return nil, false
}
ce := sv.(*ir.ConstExpr)
return ce.Val(), true
}

// isSameLiteral checks to see if 'v1' and 'v2' correspond to the same
// literal value, or if they are both nil.
func isSameLiteral(v1, v2 constant.Value) bool {
if v1 == nil && v2 == nil {
return true
}
if v1 == nil || v2 == nil {
return false
}
return constant.Compare(v1, token.EQL, v2)
}

func isConcreteConvIface(n ir.Node) bool {
sv := ir.StaticValue(n)
if sv.Op() != ir.OCONVIFACE {
return false
}
return !sv.(*ir.ConvExpr).X.Type().IsInterface()
}

func isSameFuncName(v1, v2 *ir.Name) bool {
// NB: there are a few corner cases where pointer equality
// doesn't work here, but this should be good enough for
// our purposes here.
return v1 == v2
}
2 changes: 1 addition & 1 deletion src/cmd/compile/internal/inline/inlheur/funcprops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func TestFuncProperties(t *testing.T) {
// to building a fresh compiler on the fly, or using some other
// scheme.

testcases := []string{"funcflags"}
testcases := []string{"funcflags", "returns"}

for _, tc := range testcases {
dumpfile, err := gatherPropsDumpForFile(t, tc, td)
Expand Down

0 comments on commit 3f0f767

Please sign in to comment.