blob: 6153b8c5ec7043e4812875858d6089a0765ca489 [file] [log] [blame] [edit]
// 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 devirtualize
import (
"cmd/compile/internal/base"
"cmd/compile/internal/ir"
"cmd/compile/internal/pgoir"
"cmd/compile/internal/typecheck"
"cmd/compile/internal/types"
"cmd/internal/obj"
"cmd/internal/pgo"
"cmd/internal/src"
"cmd/internal/sys"
"testing"
)
func init() {
// These are the few constants that need to be initialized in order to use
// the types package without using the typecheck package by calling
// typecheck.InitUniverse() (the normal way to initialize the types package).
types.PtrSize = 8
types.RegSize = 8
types.MaxWidth = 1 << 50
base.Ctxt = &obj.Link{Arch: &obj.LinkArch{Arch: &sys.Arch{Alignment: 1, CanMergeLoads: true}}}
typecheck.InitUniverse()
base.Debug.PGODebug = 3
}
func makePos(b *src.PosBase, line, col uint) src.XPos {
return base.Ctxt.PosTable.XPos(src.MakePos(b, line, col))
}
type profileBuilder struct {
p *pgoir.Profile
}
func newProfileBuilder() *profileBuilder {
// findHotConcreteCallee only uses pgoir.Profile.WeightedCG, so we're
// going to take a shortcut and only construct that.
return &profileBuilder{
p: &pgoir.Profile{
WeightedCG: &pgoir.IRGraph{
IRNodes: make(map[string]*pgoir.IRNode),
},
},
}
}
// Profile returns the constructed profile.
func (p *profileBuilder) Profile() *pgoir.Profile {
return p.p
}
// NewNode creates a new IRNode and adds it to the profile.
//
// fn may be nil, in which case the node will set LinkerSymbolName.
func (p *profileBuilder) NewNode(name string, fn *ir.Func) *pgoir.IRNode {
n := &pgoir.IRNode{
OutEdges: make(map[pgo.NamedCallEdge]*pgoir.IREdge),
}
if fn != nil {
n.AST = fn
} else {
n.LinkerSymbolName = name
}
p.p.WeightedCG.IRNodes[name] = n
return n
}
// Add a new call edge from caller to callee.
func addEdge(caller, callee *pgoir.IRNode, offset int, weight int64) {
namedEdge := pgo.NamedCallEdge{
CallerName: caller.Name(),
CalleeName: callee.Name(),
CallSiteOffset: offset,
}
irEdge := &pgoir.IREdge{
Src: caller,
Dst: callee,
CallSiteOffset: offset,
Weight: weight,
}
caller.OutEdges[namedEdge] = irEdge
}
// Create a new struct type named structName with a method named methName and
// return the method.
func makeStructWithMethod(pkg *types.Pkg, structName, methName string) *ir.Func {
// type structName struct{}
structType := types.NewStruct(nil)
// func (structName) methodName()
recv := types.NewField(src.NoXPos, typecheck.Lookup(structName), structType)
sig := types.NewSignature(recv, nil, nil)
fn := ir.NewFunc(src.NoXPos, src.NoXPos, pkg.Lookup(structName+"."+methName), sig)
// Add the method to the struct.
structType.SetMethods([]*types.Field{types.NewField(src.NoXPos, typecheck.Lookup(methName), sig)})
return fn
}
func TestFindHotConcreteInterfaceCallee(t *testing.T) {
p := newProfileBuilder()
pkgFoo := types.NewPkg("example.com/foo", "foo")
basePos := src.NewFileBase("foo.go", "/foo.go")
const (
// Caller start line.
callerStart = 42
// The line offset of the call we care about.
callOffset = 1
// The line offset of some other call we don't care about.
wrongCallOffset = 2
)
// type IFace interface {
// Foo()
// }
fooSig := types.NewSignature(types.FakeRecv(), nil, nil)
method := types.NewField(src.NoXPos, typecheck.Lookup("Foo"), fooSig)
iface := types.NewInterface([]*types.Field{method})
callerFn := ir.NewFunc(makePos(basePos, callerStart, 1), src.NoXPos, pkgFoo.Lookup("Caller"), types.NewSignature(nil, nil, nil))
hotCalleeFn := makeStructWithMethod(pkgFoo, "HotCallee", "Foo")
coldCalleeFn := makeStructWithMethod(pkgFoo, "ColdCallee", "Foo")
wrongLineCalleeFn := makeStructWithMethod(pkgFoo, "WrongLineCallee", "Foo")
wrongMethodCalleeFn := makeStructWithMethod(pkgFoo, "WrongMethodCallee", "Bar")
callerNode := p.NewNode("example.com/foo.Caller", callerFn)
hotCalleeNode := p.NewNode("example.com/foo.HotCallee.Foo", hotCalleeFn)
coldCalleeNode := p.NewNode("example.com/foo.ColdCallee.Foo", coldCalleeFn)
wrongLineCalleeNode := p.NewNode("example.com/foo.WrongCalleeLine.Foo", wrongLineCalleeFn)
wrongMethodCalleeNode := p.NewNode("example.com/foo.WrongCalleeMethod.Foo", wrongMethodCalleeFn)
hotMissingCalleeNode := p.NewNode("example.com/bar.HotMissingCallee.Foo", nil)
addEdge(callerNode, wrongLineCalleeNode, wrongCallOffset, 100) // Really hot, but wrong line.
addEdge(callerNode, wrongMethodCalleeNode, callOffset, 100) // Really hot, but wrong method type.
addEdge(callerNode, hotCalleeNode, callOffset, 10)
addEdge(callerNode, coldCalleeNode, callOffset, 1)
// Equal weight, but IR missing.
//
// N.B. example.com/bar sorts lexicographically before example.com/foo,
// so if the IR availability of hotCalleeNode doesn't get precedence,
// this would be mistakenly selected.
addEdge(callerNode, hotMissingCalleeNode, callOffset, 10)
// IFace.Foo()
sel := typecheck.NewMethodExpr(src.NoXPos, iface, typecheck.Lookup("Foo"))
call := ir.NewCallExpr(makePos(basePos, callerStart+callOffset, 1), ir.OCALLINTER, sel, nil)
gotFn, gotWeight := findHotConcreteInterfaceCallee(p.Profile(), callerFn, call)
if gotFn != hotCalleeFn {
t.Errorf("findHotConcreteInterfaceCallee func got %v want %v", gotFn, hotCalleeFn)
}
if gotWeight != 10 {
t.Errorf("findHotConcreteInterfaceCallee weight got %v want 10", gotWeight)
}
}
func TestFindHotConcreteFunctionCallee(t *testing.T) {
// TestFindHotConcreteInterfaceCallee already covered basic weight
// comparisons, which is shared logic. Here we just test type signature
// disambiguation.
p := newProfileBuilder()
pkgFoo := types.NewPkg("example.com/foo", "foo")
basePos := src.NewFileBase("foo.go", "/foo.go")
const (
// Caller start line.
callerStart = 42
// The line offset of the call we care about.
callOffset = 1
)
callerFn := ir.NewFunc(makePos(basePos, callerStart, 1), src.NoXPos, pkgFoo.Lookup("Caller"), types.NewSignature(nil, nil, nil))
// func HotCallee()
hotCalleeFn := ir.NewFunc(src.NoXPos, src.NoXPos, pkgFoo.Lookup("HotCallee"), types.NewSignature(nil, nil, nil))
// func WrongCallee() bool
wrongCalleeFn := ir.NewFunc(src.NoXPos, src.NoXPos, pkgFoo.Lookup("WrongCallee"), types.NewSignature(nil, nil,
[]*types.Field{
types.NewField(src.NoXPos, nil, types.Types[types.TBOOL]),
},
))
callerNode := p.NewNode("example.com/foo.Caller", callerFn)
hotCalleeNode := p.NewNode("example.com/foo.HotCallee", hotCalleeFn)
wrongCalleeNode := p.NewNode("example.com/foo.WrongCallee", wrongCalleeFn)
addEdge(callerNode, wrongCalleeNode, callOffset, 100) // Really hot, but wrong function type.
addEdge(callerNode, hotCalleeNode, callOffset, 10)
// var fn func()
name := ir.NewNameAt(src.NoXPos, typecheck.Lookup("fn"), types.NewSignature(nil, nil, nil))
// fn()
call := ir.NewCallExpr(makePos(basePos, callerStart+callOffset, 1), ir.OCALL, name, nil)
gotFn, gotWeight := findHotConcreteFunctionCallee(p.Profile(), callerFn, call)
if gotFn != hotCalleeFn {
t.Errorf("findHotConcreteFunctionCallee func got %v want %v", gotFn, hotCalleeFn)
}
if gotWeight != 10 {
t.Errorf("findHotConcreteFunctionCallee weight got %v want 10", gotWeight)
}
}