Skip to content
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

cmd/compile/internal/devirtualize: improve concrete type analysis #71935

Open
wants to merge 33 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
adc9040
cmd/compile: improve concrete type analysis for devirtualization
mateusz834 Feb 17, 2025
7f741c8
handle ranges properly
mateusz834 Feb 17, 2025
9d642a1
add test case
mateusz834 Feb 17, 2025
aaa8340
rework tests for chans
mateusz834 Feb 17, 2025
0777896
improve map tests
mateusz834 Feb 17, 2025
66d8b70
update tests
mateusz834 Feb 17, 2025
aadd746
update
mateusz834 Feb 17, 2025
d8fd8b0
add two tests and comment
mateusz834 Feb 18, 2025
e7f1037
more tests, debug messages
mateusz834 Feb 18, 2025
5d0fea2
remove testing todo
mateusz834 Feb 18, 2025
574aa22
rename test file
mateusz834 Feb 18, 2025
8cb9f5c
typos
mateusz834 Feb 18, 2025
dd2db02
update
mateusz834 Feb 18, 2025
914b14b
add nil checks for devirtualized calls
mateusz834 Feb 18, 2025
94b4282
reword comment
mateusz834 Feb 18, 2025
68f4a05
remove todos
mateusz834 Feb 18, 2025
ff52b68
rename func
mateusz834 Feb 18, 2025
a4e5a80
update comment
mateusz834 Feb 18, 2025
784e17c
keep proper line in nil panic
mateusz834 Feb 18, 2025
87c65cf
update
mateusz834 Feb 18, 2025
45c60a5
add nil panic line number test
mateusz834 Feb 18, 2025
087c347
update comment, add test case
mateusz834 Feb 18, 2025
2a83334
make tests identical
mateusz834 Feb 18, 2025
c053608
update comment
mateusz834 Feb 18, 2025
7aade42
add comment
mateusz834 Feb 18, 2025
e4cf503
remove devirtualized bool
mateusz834 Feb 19, 2025
0c57c34
update
mateusz834 Feb 21, 2025
f22918d
add more test cases
mateusz834 Feb 22, 2025
372c5bb
code tweaks
mateusz834 Feb 22, 2025
2560a2c
update
mateusz834 Feb 23, 2025
a957ba9
simplify
mateusz834 Feb 24, 2025
92b33c0
handle properly booleans from v, ok
mateusz834 Feb 24, 2025
fc782d5
add noinline for newinliner
mateusz834 Feb 24, 2025
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
331 changes: 321 additions & 10 deletions src/cmd/compile/internal/devirtualize/devirtualize.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ import (
"cmd/compile/internal/ir"
"cmd/compile/internal/typecheck"
"cmd/compile/internal/types"
"cmd/internal/src"
)

const go125ImprovedConcreteTypeAnalysis = true

// StaticCall devirtualizes the given call if possible when the concrete callee
// is available statically.
func StaticCall(call *ir.CallExpr) {
Expand All @@ -40,15 +43,31 @@ func StaticCall(call *ir.CallExpr) {
}

sel := call.Fun.(*ir.SelectorExpr)
r := ir.StaticValue(sel.X)
if r.Op() != ir.OCONVIFACE {
return
}
recv := r.(*ir.ConvExpr)
var typ *types.Type
if go125ImprovedConcreteTypeAnalysis {
typ = concreteType(sel.X)
if typ == nil {
return
}

typ := recv.X.Type()
if typ.IsInterface() {
return
// Don't try to devirtualize calls that we statically know that would have failed at runtime.
// This can happen in such case: any(0).(interface {A()}).A(), this typechecks without
// any errors, but will cause a runtime panic. We statically know that int(0) does not
// implement that interface, thus we skip the devirtualization, as it is not possible
// to make an assertion: any(0).(interface{A()}).(int) (int does not implement interface{A()}).
if !typecheck.Implements(typ, sel.X.Type()) {
return
}
} else {
r := ir.StaticValue(sel.X)
if r.Op() != ir.OCONVIFACE {
return
}
recv := r.(*ir.ConvExpr)
typ = recv.X.Type()
if typ.IsInterface() {
return
}
}

// If typ is a shape type, then it was a type argument originally
Expand Down Expand Up @@ -99,8 +118,24 @@ func StaticCall(call *ir.CallExpr) {
return
}

dt := ir.NewTypeAssertExpr(sel.Pos(), sel.X, nil)
dt.SetType(typ)
dt := ir.NewTypeAssertExpr(sel.Pos(), sel.X, typ)

if go125ImprovedConcreteTypeAnalysis {
// Consider:
//
// var v Iface
// v.A()
// v = &Impl{}
//
// Here in the devirtualizer, we determine the concrete type of v as beeing an *Impl,
// but in can still be a nil interface, we have not detected that. The v.(*Impl)
// type assertion that we make here would also have failed, but with a different
// panic "pkg.Iface is nil, not *pkg.Impl", where previously we would get a nil panic.
// We fix this, by introducing an additional nilcheck on the itab.
dt.EmitItabNilCheck = true
dt.SetPos(call.Pos())
}

x := typecheck.XDotMethod(sel.Pos(), dt, sel.Sel, true)
switch x.Op() {
case ir.ODOTMETH:
Expand Down Expand Up @@ -138,3 +173,279 @@ func StaticCall(call *ir.CallExpr) {
// Desugar OCALLMETH, if we created one (#57309).
typecheck.FixMethodCall(call)
}

const concreteTypeDebug = false

// concreteType determines the concrete type of n, following OCONVIFACEs and type asserts.
// Returns nil when the concrete type could not be determined, or when there are multiple
// (different) types assigned to an interface.
func concreteType(n ir.Node) (typ *types.Type) {
return concreteType1(n, make(map[*ir.Name]*types.Type))
}

func concreteType1(n ir.Node, analyzed map[*ir.Name]*types.Type) (typ *types.Type) {
nn := n // copy for debug messages

if concreteTypeDebug {
defer func() {
if typ == nil {
base.WarnfAt(n.Pos(), "%v concrete type not found", nn)
} else {
base.WarnfAt(n.Pos(), "%v found concrete type %v", nn, typ)
}
}()
}

for {
if concreteTypeDebug {
base.WarnfAt(n.Pos(), "%v analyzing concrete type of %v", nn, n)
}

switch n1 := n.(type) {
case *ir.ConvExpr:
// OCONVNOP might change the type, thus check whether they are identical.
if n1.Op() == ir.OCONVNOP && types.Identical(n1.Type(), n1.X.Type()) {
n = n1.X
continue
}
if n1.Op() == ir.OCONVIFACE {
n = n1.X
continue
}
case *ir.InlinedCallExpr:
if n1.Op() == ir.OINLCALL {
n = n1.SingleResult()
continue
}
case *ir.ParenExpr:
n = n1.X
continue
case *ir.TypeAssertExpr:
if !n.Type().IsInterface() {
// Asserting to a static type iface.(T), take use of that
// as this will either cause a runtime panic, or return the zero value
// of T (var v IfaceTyp; v, _ = iface.(T)).
return n.Type()
}
n = n1.X
continue
case *ir.CallExpr:
if n1.Fun != nil {
results := n1.Fun.Type().Results()
if len(results) == 1 {
retTyp := results[0].Type
if !retTyp.IsInterface() {
return retTyp
}
}
}
return nil
}

if !n.Type().IsInterface() {
return n.Type()
}

return analyzeAssignments(n, analyzed)
}
}

func analyzeAssignments(n ir.Node, analyzed map[*ir.Name]*types.Type) *types.Type {
if n.Op() != ir.ONAME {
return nil
}

name := n.(*ir.Name).Canonical()
if name.Class != ir.PAUTO {
return nil
}

if name.Op() != ir.ONAME {
base.Fatalf("reassigned %v", name)
}

if name.Addrtaken() {
return nil // conservatively assume it's reassigned with a different type indirectly
}

if typ, ok := analyzed[name]; ok {
return typ
}

// For now set the Type to nil, as we don't know it yet, we will update
// it at the end of this function, if we find a concrete type.
// This is not ideal, as in-process concreteType1 calls (that this function also
// executes) will get a nil (from the map lookup above), where we could determine the type.
analyzed[name] = nil

if concreteTypeDebug {
base.WarnfAt(name.Pos(), "analyzing assignments to %v", name)
}

// isName reports whether n is a reference to name.
isName := func(x ir.Node) bool {
if x == nil {
return false
}
n, ok := ir.OuterValue(x).(*ir.Name)
return ok && n.Canonical() == name
}

var typ *types.Type

handleType := func(dbgOp ir.Op, pos src.XPos, t *types.Type) bool {
if t == nil || t.IsInterface() {
if concreteTypeDebug {
base.WarnfAt(pos, "%v assigned (%v) with a non concrete type", name, dbgOp)
}
typ = nil
return true
}

if concreteTypeDebug {
base.WarnfAt(pos, "%v assigned (%v) with a concrete type %v", name, dbgOp, t)
}

if typ == nil || types.Identical(typ, t) {
typ = t
return false
}

// different type
typ = nil
return true
}

handleNode := func(dbgOp ir.Op, n ir.Node) bool {
if n == nil {
return false
}
if concreteTypeDebug {
base.WarnfAt(n.Pos(), "%v found assignment %v = %v (%v), analyzing the RHS node", name, name, n, dbgOp)
}
return handleType(dbgOp, n.Pos(), concreteType1(n, analyzed))
}

var do func(n ir.Node) bool
do = func(n ir.Node) bool {
switch n.Op() {
case ir.OAS:
n := n.(*ir.AssignStmt)
if isName(n.X) {
return handleNode(ir.OAS, n.Y)
}
case ir.OAS2:
n := n.(*ir.AssignListStmt)
for i, p := range n.Lhs {
if isName(p) {
return handleNode(ir.OAS2, n.Rhs[i])
}
}
case ir.OAS2DOTTYPE:
n := n.(*ir.AssignListStmt)
if isName(n.Lhs[0]) {
return handleNode(ir.OAS2DOTTYPE, n.Rhs[0])
}
if isName(n.Lhs[1]) {
// boolean, nothing to devirtualize.
typ = nil
return true
}
case ir.OAS2MAPR, ir.OAS2RECV, ir.OSELRECV2:
n := n.(*ir.AssignListStmt)
if isName(n.Lhs[0]) {
return handleType(n.Op(), n.Pos(), n.Rhs[0].Type())
}
if isName(n.Lhs[1]) {
// boolean, nothing to devirtualize.
typ = nil
return true
}
case ir.OAS2FUNC:
n := n.(*ir.AssignListStmt)
for i, p := range n.Lhs {
if isName(p) {
rhs := n.Rhs[0]
for {
if r, ok := rhs.(*ir.ParenExpr); ok {
rhs = r.X
continue
}
break
}
if call, ok := rhs.(*ir.CallExpr); ok {
retTyp := call.Fun.Type().Results()[i].Type
return handleType(ir.OAS2FUNC, n.Pos(), retTyp)
}
typ = nil
return true
}
}
case ir.ORANGE:
n := n.(*ir.RangeStmt)
xTyp := n.X.Type()

// range over an array pointer
if xTyp.IsPtr() && xTyp.Elem().IsArray() {
xTyp = xTyp.Elem()
}

if xTyp.IsArray() || xTyp.IsSlice() {
if isName(n.Key) {
// This is an index, int has no methods, so nothing to devirtualize.
typ = nil
return true
}
if isName(n.Value) {
return handleType(ir.ORANGE, n.Pos(), xTyp.Elem())
}
} else if xTyp.IsChan() {
if isName(n.Key) {
return handleType(ir.ORANGE, n.Pos(), xTyp.Elem())
}
base.Assertf(n.Value == nil, "n.Value != nil in range over chan")
} else if xTyp.IsMap() {
if isName(n.Key) {
return handleType(ir.ORANGE, n.Pos(), xTyp.Key())
}
if isName(n.Value) {
return handleType(ir.ORANGE, n.Pos(), xTyp.Elem())
}
} else if xTyp.IsInteger() || xTyp.IsString() {
// range over int/string, results do not have methods, so nothing to devirtualize.
typ = nil
return true
} else {
base.Fatalf("range over unexpected type %v", n.X.Type())
}
case ir.OSWITCH:
n := n.(*ir.SwitchStmt)
if guard, ok := n.Tag.(*ir.TypeSwitchGuard); ok {
for _, v := range n.Cases {
if v.Var == nil {
base.Assert(guard.Tag == nil)
continue
}
if isName(v.Var) {
return handleNode(v.Op(), guard.X)
}
}
}
case ir.OADDR:
n := n.(*ir.AddrExpr)
if isName(n.X) {
base.FatalfAt(n.Pos(), "%v not marked addrtaken", name)
}
case ir.OCLOSURE:
n := n.(*ir.ClosureExpr)
if ir.Any(n.Func, do) {
return true
}
}
return false
}

ir.Any(name.Curfn, do)
analyzed[name] = typ
return typ
}
3 changes: 3 additions & 0 deletions src/cmd/compile/internal/ir/expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,9 @@ type TypeAssertExpr struct {

// An internal/abi.TypeAssert descriptor to pass to the runtime.
Descriptor *obj.LSym

// Emit a nilcheck on the Itab of X.
EmitItabNilCheck bool
}

func NewTypeAssertExpr(pos src.XPos, x Node, typ *types.Type) *TypeAssertExpr {
Expand Down
1 change: 1 addition & 0 deletions src/cmd/compile/internal/noder/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -2941,6 +2941,7 @@ func (r *reader) multiExpr() []ir.Node {
as.Def = true
for i := range results {
tmp := r.temp(pos, r.typ())
tmp.Defn = as
as.PtrInit().Append(ir.NewDecl(pos, ir.ODCL, tmp))
as.Lhs.Append(tmp)

Expand Down
11 changes: 11 additions & 0 deletions src/cmd/compile/internal/ssagen/ssa.go
Original file line number Diff line number Diff line change
Expand Up @@ -5625,6 +5625,17 @@ func (s *state) dottype(n *ir.TypeAssertExpr, commaok bool) (res, resok *ssa.Val
if n.ITab != nil {
targetItab = s.expr(n.ITab)
}

if n.EmitItabNilCheck {
typs := s.f.Config.Types
iface = s.newValue2(
ssa.OpIMake,
iface.Type,
s.nilCheck(s.newValue1(ssa.OpITab, typs.BytePtr, iface)),
s.newValue1(ssa.OpIData, typs.BytePtr, iface),
)
}

return s.dottype1(n.Pos(), n.X.Type(), n.Type(), iface, nil, target, targetItab, commaok, n.Descriptor)
}

Expand Down
Loading