[dev.unified] cmd/compile: start using runtime dictionaries

This CL switches unified IR to start using runtime dictionaries,
rather than pure stenciling. In particular, for each instantiated
function `F[T]`, it now:

1. Generates a global variable `F[T]-dict` of type `[N]uintptr`, with
all of the `*runtime._type` values needed by `F[T]`.

2. Generates a function `F[T]-shaped`, with an extra
`.dict *[N]uintptr` parameter and indexing into that parameter for
derived types. (N.B., this function is not yet actually using shape
types.)

3. Changes `F[T]` to instead be a wrapper function that calls
`F[T]-shaped` passing `&F[T]-dict` as the `.dict` parameter.

This is done in one pass to make sure the overall wiring is all
working (especially, function literals and inlining).

Subsequent CLs will write more information into `F[T]-dict` and update
`F[T]-shaped` to use it instead of relying on `T`-derived information
itself. Once that's done, `F[T]-shaped` can be changed to
`F[shapify(T)]` (e.g., `F[go.shape.int]`) and deduplicated.

Change-Id: I0e802a4d9934794e01a6bfc367820af893335155
Reviewed-on: https://go-review.googlesource.com/c/go/+/420416
Run-TryBot: Matthew Dempsky <mdempsky@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Auto-Submit: Matthew Dempsky <mdempsky@google.com>
Reviewed-by: Cuong Manh Le <cuong.manhle.vn@gmail.com>
Reviewed-by: Keith Randall <khr@google.com>
Reviewed-by: Keith Randall <khr@golang.org>
Reviewed-by: David Chase <drchase@google.com>
This commit is contained in:
Matthew Dempsky 2022-07-31 18:48:16 -07:00 committed by Gopher Robot
parent 994ff78ba0
commit c9f2150cfb
5 changed files with 308 additions and 41 deletions

View file

@ -17,6 +17,7 @@ import (
"cmd/compile/internal/dwarfgen"
"cmd/compile/internal/inline"
"cmd/compile/internal/ir"
"cmd/compile/internal/objw"
"cmd/compile/internal/reflectdata"
"cmd/compile/internal/typecheck"
"cmd/compile/internal/types"
@ -61,14 +62,16 @@ func newPkgReader(pr pkgbits.PkgDecoder) *pkgReader {
// A pkgReaderIndex compactly identifies an index (and its
// corresponding dictionary) within a package's export data.
type pkgReaderIndex struct {
pr *pkgReader
idx pkgbits.Index
dict *readerDict
pr *pkgReader
idx pkgbits.Index
dict *readerDict
shapedFn *ir.Func
}
func (pri pkgReaderIndex) asReader(k pkgbits.RelocKind, marker pkgbits.SyncMarker) *reader {
r := pri.pr.newReader(k, pri.idx, marker)
r.dict = pri.dict
r.shapedFn = pri.shapedFn
return r
}
@ -98,6 +101,12 @@ type reader struct {
funarghack bool
// shapedFn is the shape-typed version of curfn, if any.
shapedFn *ir.Func
// dictParam is the .dict param, if any.
dictParam *ir.Name
// scopeVars is a stack tracking the number of variables declared in
// the current function at the moment each open scope was opened.
scopeVars []int
@ -119,7 +128,13 @@ type reader struct {
// Label to return to.
retlabel *types.Sym
inlvars, retvars ir.Nodes
// inlvars is the list of variables that the inlinee's arguments are
// assigned to, one for each receiver and normal parameter, in order.
inlvars ir.Nodes
// retvars is the list of variables that the inlinee's results are
// assigned to, one for each result parameter, in order.
retvars ir.Nodes
}
type readerDict struct {
@ -737,12 +752,7 @@ func (pr *pkgReader) objDictIdx(sym *types.Sym, idx pkgbits.Index, implicits, ex
// For stenciling, we can just skip over the type parameters.
for range dict.targs[dict.implicits:] {
// Skip past bounds without actually evaluating them.
r.Sync(pkgbits.SyncType)
if r.Bool() {
r.Len()
} else {
r.Reloc(pkgbits.RelocType)
}
r.typInfo()
}
dict.derived = make([]derivedInfo, r.Len())
@ -806,9 +816,7 @@ func (r *reader) method(rext *reader) *types.Field {
_, recv := r.param()
typ := r.signature(pkg, recv)
fnsym := sym
fnsym = ir.MethodSym(recv.Type, fnsym)
name := ir.NewNameAt(pos, fnsym)
name := ir.NewNameAt(pos, ir.MethodSym(recv.Type, sym))
setType(name, typ)
name.Func = ir.NewFunc(r.pos())
@ -981,7 +989,7 @@ var importBodyReader = map[*types.Sym]pkgReaderIndex{}
func bodyReaderFor(fn *ir.Func) (pri pkgReaderIndex, ok bool) {
if fn.Nname.Defn != nil {
pri, ok = bodyReader[fn]
assert(ok) // must always be available
base.AssertfAt(ok, base.Pos, "must have bodyReader for %v", fn) // must always be available
} else {
pri, ok = importBodyReader[fn.Sym()]
}
@ -999,7 +1007,38 @@ func (r *reader) addBody(fn *ir.Func) {
// generic functions; see comment in funcExt.
assert(fn.Nname.Defn != nil)
pri := pkgReaderIndex{r.p, r.Reloc(pkgbits.RelocBody), r.dict}
idx := r.Reloc(pkgbits.RelocBody)
var shapedFn *ir.Func
if r.hasTypeParams() && fn.OClosure == nil {
name := fn.Nname
sym := name.Sym()
shapedSym := sym.Pkg.Lookup(sym.Name + "-shaped")
// TODO(mdempsky): Once we actually start shaping functions, we'll
// need to deduplicate them.
shaped := ir.NewDeclNameAt(name.Pos(), ir.ONAME, shapedSym)
setType(shaped, shapeSig(fn, r.dict)) // TODO(mdempsky): Use shape types.
shapedFn = ir.NewFunc(fn.Pos())
shaped.Func = shapedFn
shapedFn.Nname = shaped
shapedFn.SetDupok(true)
shaped.Class = 0 // so MarkFunc doesn't complain
ir.MarkFunc(shaped)
shaped.Defn = shapedFn
shapedFn.Pragma = fn.Pragma // TODO(mdempsky): How does stencil.go handle pragmas?
typecheck.Func(shapedFn)
bodyReader[shapedFn] = pkgReaderIndex{r.p, idx, r.dict, nil}
todoBodies = append(todoBodies, shapedFn)
}
pri := pkgReaderIndex{r.p, idx, r.dict, shapedFn}
bodyReader[fn] = pri
if r.curfn == nil {
@ -1020,6 +1059,9 @@ func (pri pkgReaderIndex) funcBody(fn *ir.Func) {
func (r *reader) funcBody(fn *ir.Func) {
r.curfn = fn
r.closureVars = fn.ClosureVars
if len(r.closureVars) != 0 && r.hasTypeParams() {
r.dictParam = r.closureVars[len(r.closureVars)-1] // dictParam is last; see reader.funcLit
}
ir.WithFunc(fn, func() {
r.funcargs(fn)
@ -1028,6 +1070,11 @@ func (r *reader) funcBody(fn *ir.Func) {
return
}
if r.shapedFn != nil {
r.callShaped(fn.Pos())
return
}
body := r.stmts()
if body == nil {
body = []ir.Node{typecheck.Stmt(ir.NewBlockStmt(src.NoXPos, nil))}
@ -1039,6 +1086,139 @@ func (r *reader) funcBody(fn *ir.Func) {
r.marker.WriteTo(fn)
}
// callShaped emits a tail call to r.shapedFn, passing along the
// arguments to the current function.
func (r *reader) callShaped(pos src.XPos) {
sig := r.curfn.Nname.Type()
var args ir.Nodes
// First argument is a pointer to the -dict global variable.
args.Append(r.dictPtr())
// Collect the arguments to the current function, so we can pass
// them along to the shaped function. (This is unfortunately quite
// hairy.)
for _, params := range &types.RecvsParams {
for _, param := range params(sig).FieldSlice() {
var arg ir.Node
if param.Nname != nil {
name := param.Nname.(*ir.Name)
if !ir.IsBlank(name) {
if r.inlCall != nil {
// During inlining, we want the respective inlvar where we
// assigned the callee's arguments.
arg = r.inlvars[len(args)-1]
} else {
// Otherwise, we can use the parameter itself directly.
base.AssertfAt(name.Curfn == r.curfn, name.Pos(), "%v has curfn %v, but want %v", name, name.Curfn, r.curfn)
arg = name
}
}
}
// For anonymous and blank parameters, we don't have an *ir.Name
// to use as the argument. However, since we know the shaped
// function won't use the value either, we can just pass the
// zero value. (Also unfortunately, we don't have an easy
// zero-value IR node; so we use a default-initialized temporary
// variable.)
if arg == nil {
tmp := typecheck.TempAt(pos, r.curfn, param.Type)
r.curfn.Body.Append(
typecheck.Stmt(ir.NewDecl(pos, ir.ODCL, tmp)),
typecheck.Stmt(ir.NewAssignStmt(pos, tmp, nil)),
)
arg = tmp
}
args.Append(arg)
}
}
// Mark the function as a wrapper so it doesn't show up in stack
// traces.
r.curfn.SetWrapper(true)
call := typecheck.Call(pos, r.shapedFn.Nname, args, sig.IsVariadic()).(*ir.CallExpr)
var stmt ir.Node
if sig.NumResults() != 0 {
stmt = typecheck.Stmt(ir.NewReturnStmt(pos, []ir.Node{call}))
} else {
stmt = call
}
r.curfn.Body.Append(stmt)
}
// dictPtr returns a pointer to the runtime dictionary variable needed
// for the current function to call its shaped variant.
func (r *reader) dictPtr() ir.Node {
var fn *ir.Func
if r.inlCall != nil {
// During inlining, r.curfn is named after the caller (not the
// callee), because it's relevant to closure naming, sigh.
fn = r.inlFunc
} else {
fn = r.curfn
}
var baseSym *types.Sym
if recv := fn.Nname.Type().Recv(); recv != nil {
// All methods of a given instantiated receiver type share the
// same dictionary.
baseSym = deref(recv.Type).Sym()
} else {
baseSym = fn.Nname.Sym()
}
sym := baseSym.Pkg.Lookup(baseSym.Name + "-dict")
if sym.Def == nil {
dict := ir.NewNameAt(r.curfn.Pos(), sym)
dict.Class = ir.PEXTERN
lsym := dict.Linksym()
ot := 0
for idx, info := range r.dict.derived {
if info.needed {
typ := r.p.typIdx(typeInfo{idx: pkgbits.Index(idx), derived: true}, r.dict, false)
rtype := reflectdata.TypeLinksym(typ)
ot = objw.SymPtr(lsym, ot, rtype, 0)
} else {
// TODO(mdempsky): Compact unused runtime dictionary space.
ot = objw.Uintptr(lsym, ot, 0)
}
}
// TODO(mdempsky): Write out more dictionary information.
objw.Global(lsym, int32(ot), obj.DUPOK|obj.RODATA)
dict.SetType(r.dict.varType())
dict.SetTypecheck(1)
sym.Def = dict
}
return typecheck.Expr(ir.NewAddrExpr(r.curfn.Pos(), sym.Def.(*ir.Name)))
}
// numWords returns the number of words that dict's runtime dictionary
// variable requires.
func (dict *readerDict) numWords() int64 {
var num int
num += len(dict.derivedTypes)
// TODO(mdempsky): Add space for more dictionary information.
return int64(num)
}
// varType returns the type of dict's runtime dictionary variable.
func (dict *readerDict) varType() *types.Type {
return types.NewArray(types.Types[types.TUINTPTR], dict.numWords())
}
func (r *reader) funcargs(fn *ir.Func) {
sig := fn.Nname.Type()
@ -1096,16 +1276,20 @@ func (r *reader) funcarg(param *types.Field, sym *types.Sym, ctxt ir.Class) {
func (r *reader) addLocal(name *ir.Name, ctxt ir.Class) {
assert(ctxt == ir.PAUTO || ctxt == ir.PPARAM || ctxt == ir.PPARAMOUT)
r.Sync(pkgbits.SyncAddLocal)
if r.p.SyncMarkers() {
want := r.Int()
if have := len(r.locals); have != want {
base.FatalfAt(name.Pos(), "locals table has desynced")
if name.Sym().Name == dictParamName {
r.dictParam = name
} else {
r.Sync(pkgbits.SyncAddLocal)
if r.p.SyncMarkers() {
want := r.Int()
if have := len(r.locals); have != want {
base.FatalfAt(name.Pos(), "locals table has desynced")
}
}
r.locals = append(r.locals, name)
}
name.SetUsed(true)
r.locals = append(r.locals, name)
// TODO(mdempsky): Move earlier.
if ir.IsBlank(name) {
@ -2000,6 +2184,11 @@ func (r *reader) funcLit() ir.Node {
for len(fn.ClosureVars) < cap(fn.ClosureVars) {
ir.NewClosureVar(r.pos(), fn, r.useLocal())
}
if param := r.dictParam; param != nil {
// If we have a dictionary parameter, capture it too. For
// simplicity, we capture it last and unconditionally.
ir.NewClosureVar(param.Pos(), fn, param)
}
r.addBody(fn)
@ -2024,39 +2213,60 @@ func (r *reader) exprs() []ir.Node {
return nodes
}
// rtype returns an expression of type *runtime._type.
// dictWord returns an expression to return the specified
// uintptr-typed word from the dictionary parameter.
func (r *reader) dictWord(pos src.XPos, idx int64) ir.Node {
base.AssertfAt(r.dictParam != nil, pos, "expected dictParam in %v", r.curfn)
return typecheck.Expr(ir.NewIndexExpr(pos, r.dictParam, ir.NewBasicLit(pos, constant.MakeInt64(idx))))
}
// rtype reads a type reference from the element bitstream, and
// returns an expression of type *runtime._type representing that
// type.
func (r *reader) rtype(pos src.XPos) ir.Node {
r.Sync(pkgbits.SyncRType)
// TODO(mdempsky): For derived types, use dictionary instead.
return reflectdata.TypePtrAt(pos, r.typ())
return r.rtypeInfo(pos, r.typInfo())
}
// rtypeInfo returns an expression of type *runtime._type representing
// the given decoded type reference.
func (r *reader) rtypeInfo(pos src.XPos, info typeInfo) ir.Node {
if !info.derived {
typ := r.p.typIdx(info, r.dict, true)
return reflectdata.TypePtrAt(pos, typ)
}
return typecheck.Expr(ir.NewConvExpr(pos, ir.OCONVNOP, types.NewPtr(types.Types[types.TUINT8]), r.dictWord(pos, int64(info.idx))))
}
// convRTTI returns expressions appropriate for populating an
// ir.ConvExpr's TypeWord and SrcRType fields, respectively.
func (r *reader) convRTTI(pos src.XPos) (typeWord, srcRType ir.Node) {
r.Sync(pkgbits.SyncConvRTTI)
src := r.typ()
dst := r.typ()
srcInfo := r.typInfo()
dstInfo := r.typInfo()
dst := r.p.typIdx(dstInfo, r.dict, true)
if !dst.IsInterface() {
return
}
src := r.p.typIdx(srcInfo, r.dict, true)
// See reflectdata.ConvIfaceTypeWord.
switch {
case dst.IsEmptyInterface():
if !src.IsInterface() {
typeWord = reflectdata.TypePtrAt(pos, src) // direct eface construction
typeWord = r.rtypeInfo(pos, srcInfo) // direct eface construction
}
case !src.IsInterface():
typeWord = reflectdata.ITabAddrAt(pos, src, dst) // direct iface construction
default:
typeWord = reflectdata.TypePtrAt(pos, dst) // convI2I
typeWord = r.rtypeInfo(pos, dstInfo) // convI2I
}
// See reflectdata.ConvIfaceSrcRType.
if !src.IsInterface() {
srcRType = reflectdata.TypePtrAt(pos, src)
srcRType = r.rtypeInfo(pos, srcInfo)
}
return
@ -2096,7 +2306,7 @@ func (r *reader) exprType() ir.Node {
return n
}
rtype = lsymPtr(reflectdata.TypeLinksym(typ))
rtype = r.rtypeInfo(pos, info)
}
dt := ir.NewDynamicType(pos, rtype)
@ -2275,6 +2485,9 @@ func InlineCall(call *ir.CallExpr, fn *ir.Func, inlIndex int) *ir.InlinedCallExp
for i, cv := range r.inlFunc.ClosureVars {
r.closureVars[i] = cv.Outer
}
if len(r.closureVars) != 0 && r.hasTypeParams() {
r.dictParam = r.closureVars[len(r.closureVars)-1] // dictParam is last; see reader.funcLit
}
r.funcargs(fn)
@ -2337,8 +2550,12 @@ func InlineCall(call *ir.CallExpr, fn *ir.Func, inlIndex int) *ir.InlinedCallExp
nparams := len(r.curfn.Dcl)
ir.WithFunc(r.curfn, func() {
r.curfn.Body = r.stmts()
r.curfn.Endlineno = r.pos()
if r.shapedFn != nil {
r.callShaped(call.Pos())
} else {
r.curfn.Body = r.stmts()
r.curfn.Endlineno = r.pos()
}
// TODO(mdempsky): This shouldn't be necessary. Inlining might
// read in new function/method declarations, which could
@ -2800,3 +3017,40 @@ func setBasePos(pos src.XPos) {
// Set the position for any error messages we might print (e.g. too large types).
base.Pos = pos
}
// dictParamName is the name of the synthetic dictionary parameter
// added to shaped functions.
const dictParamName = ".dict"
// shapeSig returns a copy of fn's signature, except adding a
// dictionary parameter and promoting the receiver parameter (if any)
// to a normal parameter.
//
// The parameter types.Fields are all copied too, so their Nname
// fields can be initialized for use by the shape function.
func shapeSig(fn *ir.Func, dict *readerDict) *types.Type {
sig := fn.Nname.Type()
recv := sig.Recv()
nrecvs := 0
if recv != nil {
nrecvs++
}
params := make([]*types.Field, 1+nrecvs+sig.Params().Fields().Len())
params[0] = types.NewField(fn.Pos(), fn.Sym().Pkg.Lookup(dictParamName), types.NewPtr(dict.varType()))
if recv != nil {
params[1] = types.NewField(recv.Pos, recv.Sym, recv.Type)
}
for i, param := range sig.Params().Fields().Slice() {
d := types.NewField(param.Pos, param.Sym, param.Type)
d.SetIsDDD(param.IsDDD())
params[1+nrecvs+i] = d
}
results := make([]*types.Field, sig.Results().Fields().Len())
for i, result := range sig.Results().Fields().Slice() {
results[i] = types.NewField(result.Pos, result.Sym, result.Type)
}
return types.NewSignature(types.LocalPkg, nil, nil, params, results)
}

View file

@ -247,7 +247,7 @@ func readPackage(pr *pkgReader, importpkg *types.Pkg, localStub bool) {
path, name, code := r.p.PeekObj(idx)
if code != pkgbits.ObjStub {
objReader[types.NewPkg(path, "").Lookup(name)] = pkgReaderIndex{pr, idx, nil}
objReader[types.NewPkg(path, "").Lookup(name)] = pkgReaderIndex{pr, idx, nil, nil}
}
}
@ -271,7 +271,7 @@ func readPackage(pr *pkgReader, importpkg *types.Pkg, localStub bool) {
sym := types.NewPkg(path, "").Lookup(name)
if _, ok := importBodyReader[sym]; !ok {
importBodyReader[sym] = pkgReaderIndex{pr, idx, nil}
importBodyReader[sym] = pkgReaderIndex{pr, idx, nil, nil}
}
}

View file

@ -190,7 +190,7 @@ type writerDict struct {
// A derivedInfo represents a reference to an encoded generic Go type.
type derivedInfo struct {
idx pkgbits.Index
needed bool // TODO(mdempsky): Remove; will break x/tools importer
needed bool
}
// A typeInfo represents a reference to an encoded Go type.
@ -1952,15 +1952,26 @@ func (w *writer) exprs(exprs []syntax.Expr) {
// expression of type *runtime._type representing typ.
func (w *writer) rtype(typ types2.Type) {
w.Sync(pkgbits.SyncRType)
w.typ(typ)
w.typNeeded(typ)
}
// typNeeded writes a reference to typ, and records that its
// *runtime._type is needed.
func (w *writer) typNeeded(typ types2.Type) {
info := w.p.typIdx(typ, w.dict)
w.typInfo(info)
if info.derived {
w.dict.derived[info.idx].needed = true
}
}
// convRTTI writes information so that the reader can construct
// expressions for converting from src to dst.
func (w *writer) convRTTI(src, dst types2.Type) {
w.Sync(pkgbits.SyncConvRTTI)
w.typ(src)
w.typ(dst)
w.typNeeded(src)
w.typNeeded(dst)
}
func (w *writer) exprType(iface types2.Type, typ syntax.Expr) {
@ -1992,6 +2003,9 @@ func (w *writer) exprType(iface types2.Type, typ syntax.Expr) {
}
w.typInfo(info)
if info.derived {
w.dict.derived[info.idx].needed = true
}
}
func (dict *writerDict) methodExprIdx(recvInfo typeInfo, methodInfo selectorInfo) int {

View file

@ -76,7 +76,7 @@ func TestDebugLinesPushback(t *testing.T) {
fn := "(*List[go.shape.int_0]).PushBack"
if buildcfg.Experiment.Unified {
// Unified mangles differently
fn = "(*List[int]).PushBack"
fn = "(*List[int]).PushBack-shaped"
}
testDebugLines(t, "-N -l", "pushback.go", fn, []int{17, 18, 19, 20, 21, 22, 24}, true)
}
@ -95,7 +95,7 @@ func TestDebugLinesConvert(t *testing.T) {
fn := "G[go.shape.int_0]"
if buildcfg.Experiment.Unified {
// Unified mangles differently
fn = "G[int]"
fn = "G[int]-shaped"
}
testDebugLines(t, "-N -l", "convertline.go", fn, []int{9, 10, 11}, true)
}

View file

@ -21,7 +21,6 @@ import (
// - v1: adds the flags uint32 word
//
// TODO(mdempsky): For the next version bump:
// - remove the unused dict.derived.needed bool
// - remove the legacy "has init" bool from the public root
const currentVersion uint32 = 1