cmd/compile: reorganize init functions

Instead of writing an init function per package that does the same
thing for every package, just write that implementation once in the
runtime. Change the compiler to generate a data structure that encodes
the required initialization operations.

Reduces cmd/go binary size by 0.3%+.  Most of the init code is gone,
including all the corresponding stack map info. The .inittask
structures that replace them are quite a bit smaller.

Most usefully to me, there is no longer an init function in every -S output.
(There is an .inittask global there, but it's much less distracting.)

After this CL we could change the name of the "init.ializers" function
back to just "init".

Update #6853

R=go1.13

Change-Id: Iec82b205cc52fe3ade4d36406933c97dbc9c01b1
Reviewed-on: https://go-review.googlesource.com/c/go/+/161337
Run-TryBot: Keith Randall <khr@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Josh Bleecher Snyder <josharian@gmail.com>
This commit is contained in:
Keith Randall 2019-02-05 16:22:38 -08:00 committed by Keith Randall
parent 991c85a750
commit d949d0b925
8 changed files with 99 additions and 221 deletions

View file

@ -6,6 +6,7 @@ package gc
import ( import (
"cmd/compile/internal/types" "cmd/compile/internal/types"
"cmd/internal/obj"
) )
// A function named init is a special case. // A function named init is a special case.
@ -23,77 +24,29 @@ func renameinit() *types.Sym {
return s return s
} }
// anyinit reports whether there any interesting init statements. // fninit makes an initialization record for the package.
func anyinit(n []*Node) bool { // See runtime/proc.go:initTask for its layout.
for _, ln := range n { // The 3 tasks for initialization are:
switch ln.Op { // 1) Initialize all of the packages the current package depends on.
case ODCLFUNC, ODCLCONST, ODCLTYPE, OEMPTY: // 2) Initialize all the variables that have initializers.
case OAS: // 3) Run any init functions.
if !ln.Left.isBlank() || !candiscard(ln.Right) {
return true
}
default:
return true
}
}
// is this main
if localpkg.Name == "main" {
return true
}
// is there an explicit init function
if renameinitgen > 0 {
return true
}
// are there any imported init functions
for _, s := range types.InitSyms {
if s.Def != nil {
return true
}
}
// then none
return false
}
// fninit hand-crafts package initialization code.
//
// func init.ializers() { (0)
// <init stmts>
// }
// var initdone· uint8 (1)
// func init() { (2)
// if initdone· > 1 { (3)
// return (3a)
// }
// if initdone· == 1 { (4)
// throw() (4a)
// }
// initdone· = 1 (5)
// // over all matching imported symbols
// <pkg>.init() (6)
// init.ializers() (7)
// init.<n>() // if any (8)
// initdone· = 2 (9)
// return (10)
// }
func fninit(n []*Node) { func fninit(n []*Node) {
lineno = autogeneratedPos
nf := initfix(n) nf := initfix(n)
if !anyinit(nf) {
return var deps []*obj.LSym // initTask records for packages the current package depends on
var fns []*obj.LSym // functions to call for package initialization
// Find imported packages with init tasks.
for _, p := range types.ImportedPkgList() {
if s, ok := p.LookupOK(".inittask"); ok {
deps = append(deps, s.Linksym())
}
} }
// (0)
// Make a function that contains all the initialization statements. // Make a function that contains all the initialization statements.
// This is a separate function because we want it to appear in
// stack traces, where the init function itself does not.
var initializers *types.Sym
if len(nf) > 0 { if len(nf) > 0 {
lineno = nf[0].Pos // prolog/epilog gets line number of first init stmt lineno = nf[0].Pos // prolog/epilog gets line number of first init stmt
initializers = lookup("init.ializers") initializers := lookup("init.ializers")
disableExport(initializers) disableExport(initializers)
fn := dclfunc(initializers, nod(OTFUNC, nil, nil)) fn := dclfunc(initializers, nod(OTFUNC, nil, nil))
for _, dcl := range dummyInitFn.Func.Dcl { for _, dcl := range dummyInitFn.Func.Dcl {
@ -110,7 +63,7 @@ func fninit(n []*Node) {
typecheckslice(nf, ctxStmt) typecheckslice(nf, ctxStmt)
Curfn = nil Curfn = nil
funccompile(fn) funccompile(fn)
lineno = autogeneratedPos fns = append(fns, initializers.Linksym())
} }
if dummyInitFn.Func.Dcl != nil { if dummyInitFn.Func.Dcl != nil {
// We only generate temps using dummyInitFn if there // We only generate temps using dummyInitFn if there
@ -119,140 +72,37 @@ func fninit(n []*Node) {
Fatalf("dummyInitFn still has declarations") Fatalf("dummyInitFn still has declarations")
} }
var r []*Node // Record user init functions.
for i := 0; i < renameinitgen; i++ {
// (1) s := lookupN("init.", i)
gatevar := newname(lookup("initdone·")) fns = append(fns, s.Linksym())
addvar(gatevar, types.Types[TUINT8], PEXTERN)
// (2)
initsym := lookup("init")
fn := dclfunc(initsym, nod(OTFUNC, nil, nil))
// (3)
a := nod(OIF, nil, nil)
a.Left = nod(OGT, gatevar, nodintconst(1))
a.SetLikely(true)
r = append(r, a)
// (3a)
a.Nbody.Set1(nod(ORETURN, nil, nil))
// (4)
b := nod(OIF, nil, nil)
b.Left = nod(OEQ, gatevar, nodintconst(1))
// this actually isn't likely, but code layout is better
// like this: no JMP needed after the call.
b.SetLikely(true)
r = append(r, b)
// (4a)
b.Nbody.Set1(nod(OCALL, syslook("throwinit"), nil))
// (5)
a = nod(OAS, gatevar, nodintconst(1))
r = append(r, a)
// (6)
for _, s := range types.InitSyms {
if s == initsym {
continue
}
n := resolve(oldname(s))
if n.Op == ONONAME {
// No package-scope init function; just a
// local variable, field name, or something.
continue
}
n.checkInitFuncSignature()
a = nod(OCALL, n, nil)
r = append(r, a)
} }
// (7) if len(deps) == 0 && len(fns) == 0 && localpkg.Name != "main" && localpkg.Name != "runtime" {
if initializers != nil { return // nothing to initialize
n := newname(initializers)
addvar(n, functype(nil, nil, nil), PFUNC)
r = append(r, nod(OCALL, n, nil))
} }
// (8) // Make an .inittask structure.
sym := lookup(".inittask")
// maxInlineInitCalls is the threshold at which we switch nn := newname(sym)
// from generating calls inline to generating a static array nn.Type = types.Types[TUINT8] // dummy type
// of functions and calling them in a loop. nn.SetClass(PEXTERN)
// See CL 41500 for more discussion. sym.Def = asTypesNode(nn)
const maxInlineInitCalls = 500 exportsym(nn)
lsym := sym.Linksym()
if renameinitgen < maxInlineInitCalls { ot := 0
// Not many init functions. Just call them all directly. ot = duintptr(lsym, ot, 0) // state: not initialized yet
for i := 0; i < renameinitgen; i++ { ot = duintptr(lsym, ot, uint64(len(deps)))
s := lookupN("init.", i) ot = duintptr(lsym, ot, uint64(len(fns)))
n := asNode(s.Def) for _, d := range deps {
n.checkInitFuncSignature() ot = dsymptr(lsym, ot, d, 0)
a = nod(OCALL, n, nil)
r = append(r, a)
}
} else {
// Lots of init functions.
// Set up an array of functions and loop to call them.
// This is faster to compile and similar at runtime.
// Build type [renameinitgen]func().
typ := types.NewArray(functype(nil, nil, nil), int64(renameinitgen))
// Make and fill array.
fnarr := staticname(typ)
fnarr.Name.SetReadonly(true)
for i := 0; i < renameinitgen; i++ {
s := lookupN("init.", i)
lhs := nod(OINDEX, fnarr, nodintconst(int64(i)))
rhs := asNode(s.Def)
rhs.checkInitFuncSignature()
as := nod(OAS, lhs, rhs)
as = typecheck(as, ctxStmt)
genAsStatic(as)
}
// Generate a loop that calls each function in turn.
// for i := 0; i < renameinitgen; i++ {
// fnarr[i]()
// }
i := temp(types.Types[TINT])
fnidx := nod(OINDEX, fnarr, i)
fnidx.SetBounded(true)
zero := nod(OAS, i, nodintconst(0))
cond := nod(OLT, i, nodintconst(int64(renameinitgen)))
incr := nod(OAS, i, nod(OADD, i, nodintconst(1)))
body := nod(OCALL, fnidx, nil)
loop := nod(OFOR, cond, incr)
loop.Nbody.Set1(body)
loop.Ninit.Set1(zero)
loop = typecheck(loop, ctxStmt)
r = append(r, loop)
} }
for _, f := range fns {
// (9) ot = dsymptr(lsym, ot, f, 0)
a = nod(OAS, gatevar, nodintconst(2)) }
// An initTask has pointers, but none into the Go heap.
r = append(r, a) // It's not quite read only, the state field must be modifiable.
ggloblsym(lsym, int32(ot), obj.NOPTR)
// (10)
a = nod(ORETURN, nil, nil)
r = append(r, a)
exportsym(fn.Func.Nname)
fn.Nbody.Set(r)
funcbody()
Curfn = fn
fn = typecheck(fn, ctxStmt)
typecheckslice(r, ctxStmt)
Curfn = nil
funccompile(fn)
} }
func (n *Node) checkInitFuncSignature() { func (n *Node) checkInitFuncSignature() {

View file

@ -148,7 +148,7 @@ func relocsym(ctxt *Link, s *sym.Symbol) {
// When putting the runtime but not main into a shared library // When putting the runtime but not main into a shared library
// these symbols are undefined and that's OK. // these symbols are undefined and that's OK.
if ctxt.BuildMode == BuildModeShared { if ctxt.BuildMode == BuildModeShared {
if r.Sym.Name == "main.main" || r.Sym.Name == "main.init" { if r.Sym.Name == "main.main" || r.Sym.Name == "main..inittask" {
r.Sym.Type = sym.SDYNIMPORT r.Sym.Type = sym.SDYNIMPORT
} else if strings.HasPrefix(r.Sym.Name, "go.info.") { } else if strings.HasPrefix(r.Sym.Name, "go.info.") {
// Skip go.info symbols. They are only needed to communicate // Skip go.info symbols. They are only needed to communicate

View file

@ -222,7 +222,7 @@ func (d *deadcodepass) init() {
// functions and mark what is reachable from there. // functions and mark what is reachable from there.
if d.ctxt.linkShared && (d.ctxt.BuildMode == BuildModeExe || d.ctxt.BuildMode == BuildModePIE) { if d.ctxt.linkShared && (d.ctxt.BuildMode == BuildModeExe || d.ctxt.BuildMode == BuildModePIE) {
names = append(names, "main.main", "main.init") names = append(names, "main.main", "main..inittask")
} else { } else {
// The external linker refers main symbol directly. // The external linker refers main symbol directly.
if d.ctxt.LinkMode == LinkExternal && (d.ctxt.BuildMode == BuildModeExe || d.ctxt.BuildMode == BuildModePIE) { if d.ctxt.LinkMode == LinkExternal && (d.ctxt.BuildMode == BuildModeExe || d.ctxt.BuildMode == BuildModePIE) {
@ -234,7 +234,7 @@ func (d *deadcodepass) init() {
} }
names = append(names, *flagEntrySymbol) names = append(names, *flagEntrySymbol)
if d.ctxt.BuildMode == BuildModePlugin { if d.ctxt.BuildMode == BuildModePlugin {
names = append(names, objabi.PathToPrefix(*flagPluginPath)+".init", objabi.PathToPrefix(*flagPluginPath)+".main", "go.plugin.tabs") names = append(names, objabi.PathToPrefix(*flagPluginPath)+"..inittask", objabi.PathToPrefix(*flagPluginPath)+".main", "go.plugin.tabs")
// We don't keep the go.plugin.exports symbol, // We don't keep the go.plugin.exports symbol,
// but we do keep the symbols it refers to. // but we do keep the symbols it refers to.

View file

@ -92,15 +92,13 @@ func open(name string) (*Plugin, error) {
plugins[filepath] = p plugins[filepath] = p
pluginsMu.Unlock() pluginsMu.Unlock()
initStr := make([]byte, len(pluginpath)+6) initStr := make([]byte, len(pluginpath)+len("..inittask")+1) // +1 for terminating NUL
copy(initStr, pluginpath) copy(initStr, pluginpath)
copy(initStr[len(pluginpath):], ".init") copy(initStr[len(pluginpath):], "..inittask")
initFuncPC := C.pluginLookup(h, (*C.char)(unsafe.Pointer(&initStr[0])), &cErr) initTask := C.pluginLookup(h, (*C.char)(unsafe.Pointer(&initStr[0])), &cErr)
if initFuncPC != nil { if initTask != nil {
initFuncP := &initFuncPC doInit(initTask)
initFunc := *(*func())(unsafe.Pointer(&initFuncP))
initFunc()
} }
// Fill out the value of each plugin symbol. // Fill out the value of each plugin symbol.
@ -150,3 +148,7 @@ var (
// lastmoduleinit is defined in package runtime // lastmoduleinit is defined in package runtime
func lastmoduleinit() (pluginpath string, syms map[string]interface{}, errstr string) func lastmoduleinit() (pluginpath string, syms map[string]interface{}, errstr string)
// doInit is defined in package runtime
//go:linkname doInit runtime.doInit
func doInit(t unsafe.Pointer) // t should be a *runtime.initTask

View file

@ -183,10 +183,6 @@ func panicmem() {
panic(memoryError) panic(memoryError)
} }
func throwinit() {
throw("recursive call during initialization - linker skew")
}
// Create a new deferred function fn with siz bytes of arguments. // Create a new deferred function fn with siz bytes of arguments.
// The compiler turns a defer statement into a call to this. // The compiler turns a defer statement into a call to this.
//go:nosplit //go:nosplit

View file

@ -82,11 +82,11 @@ var (
raceprocctx0 uintptr raceprocctx0 uintptr
) )
//go:linkname runtime_init runtime.init //go:linkname runtime_inittask runtime..inittask
func runtime_init() var runtime_inittask initTask
//go:linkname main_init main.init //go:linkname main_inittask main..inittask
func main_init() var main_inittask initTask
// main_init_done is a signal used by cgocallbackg that initialization // main_init_done is a signal used by cgocallbackg that initialization
// has been completed. It is made before _cgo_notify_runtime_init_done, // has been completed. It is made before _cgo_notify_runtime_init_done,
@ -144,7 +144,7 @@ func main() {
throw("runtime.main not on m0") throw("runtime.main not on m0")
} }
runtime_init() // must be before defer doInit(&runtime_inittask) // must be before defer
if nanotime() == 0 { if nanotime() == 0 {
throw("nanotime returning zero") throw("nanotime returning zero")
} }
@ -184,8 +184,8 @@ func main() {
cgocall(_cgo_notify_runtime_init_done, nil) cgocall(_cgo_notify_runtime_init_done, nil)
} }
fn := main_init // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime doInit(&main_inittask)
fn()
close(main_init_done) close(main_init_done)
needUnlock = false needUnlock = false
@ -196,7 +196,7 @@ func main() {
// has a main, but it is not executed. // has a main, but it is not executed.
return return
} }
fn = main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
fn() fn()
if raceenabled { if raceenabled {
racefini() racefini()
@ -5185,3 +5185,35 @@ func gcd(a, b uint32) uint32 {
} }
return a return a
} }
// An initTask represents the set of initializations that need to be done for a package.
type initTask struct {
// TODO: pack the first 3 fields more tightly?
state uintptr // 0 = uninitialized, 1 = in progress, 2 = done
ndeps uintptr
nfns uintptr
// followed by ndeps instances of an *initTask, one per package depended on
// followed by nfns pcs, one per init function to run
}
func doInit(t *initTask) {
switch t.state {
case 2: // fully initialized
return
case 1: // initialization in progress
throw("recursive call during initialization - linker skew")
default: // not initialized yet
t.state = 1 // initialization in progress
for i := uintptr(0); i < t.ndeps; i++ {
p := add(unsafe.Pointer(t), (3+i)*sys.PtrSize)
t2 := *(**initTask)(p)
doInit(t2)
}
for i := uintptr(0); i < t.nfns; i++ {
p := add(unsafe.Pointer(t), (3+t.ndeps+i)*sys.PtrSize)
f := *(*func())(unsafe.Pointer(&p))
f()
}
t.state = 2 // initialization done
}
}

View file

@ -65,8 +65,8 @@ func f() int {
panic("traceback truncated after init.ializers") panic("traceback truncated after init.ializers")
} }
f, _ = iter.Next() f, _ = iter.Next()
if f.Function != "runtime.main" { if !strings.HasPrefix(f.Function, "runtime.") {
panic("runtime.main missing") panic("runtime. driver missing")
} }
return 0 return 0

View file

@ -9,13 +9,11 @@
package main package main
import "runtime"
func init() { func init() {
} }
func main() { func main() {
init() // ERROR "undefined.*init" init() // ERROR "undefined.*init"
runtime.init() // ERROR "unexported.*runtime\.init" runtime.init() // ERROR "undefined.*runtime\.init"
var _ = init // ERROR "undefined.*init" var _ = init // ERROR "undefined.*init"
} }