[dev.typeparams] cmd/compile: simplify SSA devirtualization

This CL implements a few improvements to SSA devirtualization to make
it simpler and more general:

1. Change reflectdata.ITabAddr to now immediately generate the wrapper
functions and write out the itab symbol data. Previously, these were
each handled by separate phases later on.

2. Removes the hack in typecheck where we marked itabs that we
expected to need later. Instead, the calls to ITabAddr in walk now
handle generating the wrappers.

3. Changes the SSA interface call devirtualization algorithm to just
use the itab symbol data (namely, its relocations) to figure out what
pointer is available in memory at the given offset. This decouples it
somewhat from reflectdata.

Change-Id: I8fe06922af8f8a1e7c93f5aff2b60ff59b8e7114
Reviewed-on: https://go-review.googlesource.com/c/go/+/327871
Run-TryBot: Matthew Dempsky <mdempsky@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Trust: Matthew Dempsky <mdempsky@google.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
This commit is contained in:
Matthew Dempsky 2021-06-14 16:26:26 -07:00
parent 132ea56d29
commit dd95a4e3db
9 changed files with 92 additions and 189 deletions

View file

@ -673,7 +673,7 @@ func (e *escape) exprSkipInit(k hole, n ir.Node) {
n := n.(*ir.BinaryExpr) n := n.(*ir.BinaryExpr)
// Note: n.X is not needed because it can never point to memory that might escape. // Note: n.X is not needed because it can never point to memory that might escape.
e.expr(k, n.Y) e.expr(k, n.Y)
case ir.OIDATA: case ir.OIDATA, ir.OSPTR:
n := n.(*ir.UnaryExpr) n := n.(*ir.UnaryExpr)
e.expr(k, n.X) e.expr(k, n.X)
case ir.OSLICE2ARRPTR: case ir.OSLICE2ARRPTR:

View file

@ -181,7 +181,6 @@ func Main(archInit func(*ssagen.ArchInfo)) {
typecheck.Target = new(ir.Package) typecheck.Target = new(ir.Package)
typecheck.NeedITab = func(t, iface *types.Type) { reflectdata.ITabAddr(t, iface) }
typecheck.NeedRuntimeType = reflectdata.NeedRuntimeType // TODO(rsc): TypeSym for lock? typecheck.NeedRuntimeType = reflectdata.NeedRuntimeType // TODO(rsc): TypeSym for lock?
base.AutogeneratedPos = makePos(src.NewFileBase("<autogenerated>", "<autogenerated>"), 1, 0) base.AutogeneratedPos = makePos(src.NewFileBase("<autogenerated>", "<autogenerated>"), 1, 0)
@ -193,6 +192,11 @@ func Main(archInit func(*ssagen.ArchInfo)) {
dwarfgen.RecordPackageName() dwarfgen.RecordPackageName()
// Prepare for backend processing. This must happen before pkginit,
// because it generates itabs for initializing global variables.
typecheck.InitRuntime()
ssagen.InitConfig()
// Build init task. // Build init task.
if initTask := pkginit.Task(); initTask != nil { if initTask := pkginit.Task(); initTask != nil {
typecheck.Export(initTask) typecheck.Export(initTask)
@ -252,6 +256,11 @@ func Main(archInit func(*ssagen.ArchInfo)) {
base.Timer.Start("fe", "escapes") base.Timer.Start("fe", "escapes")
escape.Funcs(typecheck.Target.Decls) escape.Funcs(typecheck.Target.Decls)
// TODO(mdempsky): This is a hack. We need a proper, global work
// queue for scheduling function compilation so components don't
// need to adjust their behavior depending on when they're called.
reflectdata.AfterGlobalEscapeAnalysis = true
// Collect information for go:nowritebarrierrec // Collect information for go:nowritebarrierrec
// checking. This must happen before transforming closures during Walk // checking. This must happen before transforming closures during Walk
// We'll do the final check after write barriers are // We'll do the final check after write barriers are
@ -260,17 +269,7 @@ func Main(archInit func(*ssagen.ArchInfo)) {
ssagen.EnableNoWriteBarrierRecCheck() ssagen.EnableNoWriteBarrierRecCheck()
} }
// Prepare for SSA compilation.
// This must be before CompileITabs, because CompileITabs
// can trigger function compilation.
typecheck.InitRuntime()
ssagen.InitConfig()
// Just before compilation, compile itabs found on
// the right side of OCONVIFACE so that methods
// can be de-virtualized during compilation.
ir.CurFunc = nil ir.CurFunc = nil
reflectdata.CompileITabs()
// Compile top level functions. // Compile top level functions.
// Don't use range--walk can add functions to Target.Decls. // Don't use range--walk can add functions to Target.Decls.

View file

@ -117,7 +117,7 @@ func dumpdata() {
addsignats(typecheck.Target.Externs) addsignats(typecheck.Target.Externs)
reflectdata.WriteRuntimeTypes() reflectdata.WriteRuntimeTypes()
reflectdata.WriteTabs() reflectdata.WriteTabs()
numPTabs, numITabs := reflectdata.CountTabs() numPTabs := reflectdata.CountPTabs()
reflectdata.WriteImportStrings() reflectdata.WriteImportStrings()
reflectdata.WriteBasicTypes() reflectdata.WriteBasicTypes()
dumpembeds() dumpembeds()
@ -158,13 +158,10 @@ func dumpdata() {
if numExports != len(typecheck.Target.Exports) { if numExports != len(typecheck.Target.Exports) {
base.Fatalf("Target.Exports changed after compile functions loop") base.Fatalf("Target.Exports changed after compile functions loop")
} }
newNumPTabs, newNumITabs := reflectdata.CountTabs() newNumPTabs := reflectdata.CountPTabs()
if newNumPTabs != numPTabs { if newNumPTabs != numPTabs {
base.Fatalf("ptabs changed after compile functions loop") base.Fatalf("ptabs changed after compile functions loop")
} }
if newNumITabs != numITabs {
base.Fatalf("itabs changed after compile functions loop")
}
} }
func dumpLinkerObj(bout *bio.Writer) { func dumpLinkerObj(bout *bio.Writer) {

View file

@ -28,23 +28,13 @@ import (
"cmd/internal/src" "cmd/internal/src"
) )
type itabEntry struct {
t, itype *types.Type
lsym *obj.LSym // symbol of the itab itself
// symbols of each method in
// the itab, sorted by byte offset;
// filled in by CompileITabs
entries []*obj.LSym
}
type ptabEntry struct { type ptabEntry struct {
s *types.Sym s *types.Sym
t *types.Type t *types.Type
} }
func CountTabs() (numPTabs, numITabs int) { func CountPTabs() int {
return len(ptabs), len(itabs) return len(ptabs)
} }
// runtime interface and reflection data structures // runtime interface and reflection data structures
@ -56,7 +46,6 @@ var (
gcsymmu sync.Mutex // protects gcsymset and gcsymslice gcsymmu sync.Mutex // protects gcsymset and gcsymslice
gcsymset = make(map[*types.Type]struct{}) gcsymset = make(map[*types.Type]struct{})
itabs []itabEntry
ptabs []*ir.Name ptabs []*ir.Name
) )
@ -841,16 +830,16 @@ func TypePtr(t *types.Type) *ir.AddrExpr {
return typecheck.Expr(typecheck.NodAddr(n)).(*ir.AddrExpr) return typecheck.Expr(typecheck.NodAddr(n)).(*ir.AddrExpr)
} }
func ITabAddr(t, itype *types.Type) *ir.AddrExpr { // ITabAddr returns an expression representing a pointer to the itab
if t == nil || (t.IsPtr() && t.Elem() == nil) || t.IsUntyped() || !itype.IsInterface() || itype.IsEmptyInterface() { // for concrete type typ implementing interface iface.
base.Fatalf("ITabAddr(%v, %v)", t, itype) func ITabAddr(typ, iface *types.Type) *ir.AddrExpr {
} s, existed := ir.Pkgs.Itab.LookupOK(typ.ShortString() + "," + iface.ShortString())
s, existed := ir.Pkgs.Itab.LookupOK(t.ShortString() + "," + itype.ShortString()) lsym := s.Linksym()
if !existed { if !existed {
itabs = append(itabs, itabEntry{t: t, itype: itype, lsym: s.Linksym()}) writeITab(lsym, typ, iface)
} }
lsym := s.Linksym()
n := ir.NewLinksymExpr(base.Pos, lsym, types.Types[types.TUINT8]) n := ir.NewLinksymExpr(base.Pos, lsym, types.Types[types.TUINT8])
return typecheck.Expr(typecheck.NodAddr(n)).(*ir.AddrExpr) return typecheck.Expr(typecheck.NodAddr(n)).(*ir.AddrExpr)
} }
@ -1223,83 +1212,6 @@ func InterfaceMethodOffset(ityp *types.Type, i int64) int64 {
return int64(commonSize()+4*types.PtrSize+uncommonSize(ityp)) + i*8 return int64(commonSize()+4*types.PtrSize+uncommonSize(ityp)) + i*8
} }
// for each itabEntry, gather the methods on
// the concrete type that implement the interface
func CompileITabs() {
for i := range itabs {
tab := &itabs[i]
methods := genfun(tab.t, tab.itype)
if len(methods) == 0 {
continue
}
tab.entries = methods
}
}
// for the given concrete type and interface
// type, return the (sorted) set of methods
// on the concrete type that implement the interface
func genfun(t, it *types.Type) []*obj.LSym {
if t == nil || it == nil {
return nil
}
sigs := imethods(it)
methods := methods(t)
out := make([]*obj.LSym, 0, len(sigs))
// TODO(mdempsky): Short circuit before calling methods(t)?
// See discussion on CL 105039.
if len(sigs) == 0 {
return nil
}
// both sigs and methods are sorted by name,
// so we can find the intersect in a single pass
for _, m := range methods {
if m.name == sigs[0].name {
out = append(out, m.isym)
sigs = sigs[1:]
if len(sigs) == 0 {
break
}
}
}
if len(sigs) != 0 {
base.Fatalf("incomplete itab")
}
return out
}
// ITabSym uses the information gathered in
// CompileITabs to de-virtualize interface methods.
// Since this is called by the SSA backend, it shouldn't
// generate additional Nodes, Syms, etc.
func ITabSym(it *obj.LSym, offset int64) *obj.LSym {
var syms []*obj.LSym
if it == nil {
return nil
}
for i := range itabs {
e := &itabs[i]
if e.lsym == it {
syms = e.entries
break
}
}
if syms == nil {
return nil
}
// keep this arithmetic in sync with *itab layout
methodnum := int((offset - 2*int64(types.PtrSize) - 8) / int64(types.PtrSize))
if methodnum >= len(syms) {
return nil
}
return syms[methodnum]
}
// NeedRuntimeType ensures that a runtime type descriptor is emitted for t. // NeedRuntimeType ensures that a runtime type descriptor is emitted for t.
func NeedRuntimeType(t *types.Type) { func NeedRuntimeType(t *types.Type) {
if t.HasTParam() { if t.HasTParam() {
@ -1346,9 +1258,36 @@ func WriteRuntimeTypes() {
} }
} }
func WriteTabs() { // writeITab writes the itab for concrete type typ implementing
// process itabs // interface iface.
for _, i := range itabs { func writeITab(lsym *obj.LSym, typ, iface *types.Type) {
// TODO(mdempsky): Fix methodWrapper, geneq, and genhash (and maybe
// others) to stop clobbering these.
oldpos, oldfn := base.Pos, ir.CurFunc
defer func() { base.Pos, ir.CurFunc = oldpos, oldfn }()
if typ == nil || (typ.IsPtr() && typ.Elem() == nil) || typ.IsUntyped() || iface == nil || !iface.IsInterface() || iface.IsEmptyInterface() {
base.Fatalf("writeITab(%v, %v)", typ, iface)
}
sigs := iface.AllMethods().Slice()
entries := make([]*obj.LSym, 0, len(sigs))
// both sigs and methods are sorted by name,
// so we can find the intersection in a single pass
for _, m := range methods(typ) {
if m.name == sigs[0].Sym {
entries = append(entries, m.isym)
sigs = sigs[1:]
if len(sigs) == 0 {
break
}
}
}
if len(sigs) != 0 {
base.Fatalf("incomplete itab")
}
// dump empty itab symbol into i.sym // dump empty itab symbol into i.sym
// type itab struct { // type itab struct {
// inter *interfacetype // inter *interfacetype
@ -1357,18 +1296,19 @@ func WriteTabs() {
// _ [4]byte // _ [4]byte
// fun [1]uintptr // variable sized // fun [1]uintptr // variable sized
// } // }
o := objw.SymPtr(i.lsym, 0, writeType(i.itype), 0) o := objw.SymPtr(lsym, 0, writeType(iface), 0)
o = objw.SymPtr(i.lsym, o, writeType(i.t), 0) o = objw.SymPtr(lsym, o, writeType(typ), 0)
o = objw.Uint32(i.lsym, o, types.TypeHash(i.t)) // copy of type hash o = objw.Uint32(lsym, o, types.TypeHash(typ)) // copy of type hash
o += 4 // skip unused field o += 4 // skip unused field
for _, fn := range genfun(i.t, i.itype) { for _, fn := range entries {
o = objw.SymPtrWeak(i.lsym, o, fn, 0) // method pointer for each method o = objw.SymPtrWeak(lsym, o, fn, 0) // method pointer for each method
} }
// Nothing writes static itabs, so they are read only. // Nothing writes static itabs, so they are read only.
objw.Global(i.lsym, int32(o), int16(obj.DUPOK|obj.RODATA)) objw.Global(lsym, int32(o), int16(obj.DUPOK|obj.RODATA))
i.lsym.Set(obj.AttrContentAddressable, true) lsym.Set(obj.AttrContentAddressable, true)
} }
func WriteTabs() {
// process ptabs // process ptabs
if types.LocalPkg.Name == "main" && len(ptabs) > 0 { if types.LocalPkg.Name == "main" && len(ptabs) > 0 {
ot := 0 ot := 0
@ -1926,20 +1866,10 @@ func methodWrapper(rcvr *types.Type, method *types.Field, forItab bool) *obj.LSy
ir.CurFunc = fn ir.CurFunc = fn
typecheck.Stmts(fn.Body) typecheck.Stmts(fn.Body)
// TODO(mdempsky): Make this unconditional. The exporter now if AfterGlobalEscapeAnalysis {
// includes all of the inline bodies we need, and the "importedType"
// logic above now correctly suppresses compiling out-of-package
// types that we might not have inline bodies for. The only problem
// now is that the extra inlining can now introduce further new
// itabs, and gc.dumpdata's ad hoc compile loop doesn't handle this.
//
// CL 327871 will address this by writing itabs and generating
// wrappers as part of the loop, so we won't have to worry about
// "itabs changed after compile functions loop" errors anymore.
if rcvr.IsPtr() && rcvr.Elem() == method.Type.Recv().Type && rcvr.Elem().Sym() != nil {
inline.InlineCalls(fn) inline.InlineCalls(fn)
}
escape.Batch([]*ir.Func{fn}, false) escape.Batch([]*ir.Func{fn}, false)
}
ir.CurFunc = nil ir.CurFunc = nil
typecheck.Target.Decls = append(typecheck.Target.Decls, fn) typecheck.Target.Decls = append(typecheck.Target.Decls, fn)
@ -1947,6 +1877,12 @@ func methodWrapper(rcvr *types.Type, method *types.Field, forItab bool) *obj.LSy
return lsym return lsym
} }
// AfterGlobalEscapeAnalysis tracks whether package gc has already
// performed the main, global escape analysis pass. If so,
// methodWrapper takes responsibility for escape analyzing any
// generated wrappers.
var AfterGlobalEscapeAnalysis bool
var ZeroSize int64 var ZeroSize int64
// MarkTypeUsedInInterface marks that type t is converted to an interface. // MarkTypeUsedInInterface marks that type t is converted to an interface.

View file

@ -149,12 +149,6 @@ type Frontend interface {
// for the parts of that compound type. // for the parts of that compound type.
SplitSlot(parent *LocalSlot, suffix string, offset int64, t *types.Type) LocalSlot SplitSlot(parent *LocalSlot, suffix string, offset int64, t *types.Type) LocalSlot
// DerefItab dereferences an itab function
// entry, given the symbol of the itab and
// the byte offset of the function pointer.
// It may return nil.
DerefItab(sym *obj.LSym, offset int64) *obj.LSym
// Line returns a string describing the given position. // Line returns a string describing the given position.
Line(src.XPos) string Line(src.XPos) string

View file

@ -745,28 +745,22 @@ func uaddOvf(a, b int64) bool {
return uint64(a)+uint64(b) < uint64(a) return uint64(a)+uint64(b) < uint64(a)
} }
// de-virtualize an InterCall // loadLSymOffset simulates reading a word at an offset into a
// 'sym' is the symbol for the itab // read-only symbol's runtime memory. If it would read a pointer to
func devirt(v *Value, aux Aux, sym Sym, offset int64) *AuxCall { // another symbol, that symbol is returned. Otherwise, it returns nil.
f := v.Block.Func func loadLSymOffset(lsym *obj.LSym, offset int64) *obj.LSym {
n, ok := sym.(*obj.LSym) if lsym.Type != objabi.SRODATA {
if !ok {
return nil return nil
} }
lsym := f.fe.DerefItab(n, offset)
if f.pass.debug > 0 { for _, r := range lsym.R {
if lsym != nil { if int64(r.Off) == offset && r.Type&^objabi.R_WEAK == objabi.R_ADDR && r.Add == 0 {
f.Warnl(v.Pos, "de-virtualizing call") return r.Sym
} else {
f.Warnl(v.Pos, "couldn't de-virtualize call")
} }
} }
if lsym == nil {
return nil return nil
} }
va := aux.(*AuxCall)
return StaticAuxCall(lsym, va.abiInfo)
}
// de-virtualize an InterLECall // de-virtualize an InterLECall
// 'sym' is the symbol for the itab // 'sym' is the symbol for the itab
@ -776,18 +770,14 @@ func devirtLESym(v *Value, aux Aux, sym Sym, offset int64) *obj.LSym {
return nil return nil
} }
f := v.Block.Func lsym := loadLSymOffset(n, offset)
lsym := f.fe.DerefItab(n, offset) if f := v.Block.Func; f.pass.debug > 0 {
if f.pass.debug > 0 {
if lsym != nil { if lsym != nil {
f.Warnl(v.Pos, "de-virtualizing call") f.Warnl(v.Pos, "de-virtualizing call")
} else { } else {
f.Warnl(v.Pos, "couldn't de-virtualize call") f.Warnl(v.Pos, "couldn't de-virtualize call")
} }
} }
if lsym == nil {
return nil
}
return lsym return lsym
} }

View file

@ -7401,10 +7401,6 @@ func (e *ssafn) Auto(pos src.XPos, t *types.Type) *ir.Name {
return typecheck.TempAt(pos, e.curfn, t) // Note: adds new auto to e.curfn.Func.Dcl list return typecheck.TempAt(pos, e.curfn, t) // Note: adds new auto to e.curfn.Func.Dcl list
} }
func (e *ssafn) DerefItab(it *obj.LSym, offset int64) *obj.LSym {
return reflectdata.ITabSym(it, offset)
}
// SplitSlot returns a slot representing the data of parent starting at offset. // SplitSlot returns a slot representing the data of parent starting at offset.
func (e *ssafn) SplitSlot(parent *ssa.LocalSlot, suffix string, offset int64, t *types.Type) ssa.LocalSlot { func (e *ssafn) SplitSlot(parent *ssa.LocalSlot, suffix string, offset int64, t *types.Type) ssa.LocalSlot {
node := parent.N node := parent.N

View file

@ -379,14 +379,6 @@ func Assignop(src, dst *types.Type) (ir.Op, string) {
var missing, have *types.Field var missing, have *types.Field
var ptr int var ptr int
if implements(src, dst, &missing, &have, &ptr) { if implements(src, dst, &missing, &have, &ptr) {
// Call NeedITab/ITabAddr so that (src, dst)
// gets added to itabs early, which allows
// us to de-virtualize calls through this
// type/interface pair later. See CompileITabs in reflect.go
if types.IsDirectIface(src) && !dst.IsEmptyInterface() {
NeedITab(src, dst)
}
return ir.OCONVIFACE, "" return ir.OCONVIFACE, ""
} }

View file

@ -24,7 +24,6 @@ var inimport bool // set during import
var TypecheckAllowed bool var TypecheckAllowed bool
var ( var (
NeedITab = func(t, itype *types.Type) {}
NeedRuntimeType = func(*types.Type) {} NeedRuntimeType = func(*types.Type) {}
) )