Commit 862b9ddd authored by David Crawshaw's avatar David Crawshaw

cmd/link: prune unused methods

Today the linker keeps all methods of reachable types. This is
necessary if a program uses reflect.Value.Call. But while use of
reflection is widespread in Go for encoders and decoders, using
it to call a method is rare.

This CL looks for the use of reflect.Value.Call in a program, and
if it is absent, adopts a (reasonably conservative) method pruning
strategy as part of dead code elimination. Any method that is
directly called is kept, and any method that matches a used
interface's method signature is kept.

Whether or not a method body is kept is determined by the relocation
from its receiver's *rtype to its *rtype. A small change in the
compiler marks these relocations as R_METHOD so they can be easily
collected and manipulated by the linker.

As a bonus, this technique removes the text segment of methods that
have been inlined. Looking at the output of building cmd/objdump with
-ldflags=-v=2 shows that inlined methods like
runtime.(*traceAllocBlockPtr).ptr are removed from the program.

Relatively little work is necessary to do this. Linking two
examples, jujud and cmd/objdump show no more than +2% link time.

Binaries that do not use reflect.Call.Value drop 4 - 20% in size:

	addr2line: -793KB (18%)
	asm:       -346KB (8%)
	cgo:       -490KB (10%)
	compile:   -564KB (4%)
	dist:      -736KB (17%)
	fix:       -404KB (12%)
	link:      -328KB (7%)
	nm:        -827KB (19%)
	objdump:   -712KB (16%)
	pack:      -327KB (14%)
	yacc:      -350KB (10%)

Binaries that do use reflect.Call.Value see a modest size decrease
of 2 - 6% thanks to pruning of unexported methods:

	api:    -151KB (3%)
	cover:  -222KB (4%)
	doc:    -106KB (2.5%)
	pprof:  -314KB (3%)
	trace:  -357KB (4%)
	vet:    -187KB (2.7%)
	jujud:  -4.4MB (5.8%)
	cmd/go: -384KB (3.4%)

The trivial Hello example program goes from 2MB to 1.68MB:

	package main

	import "fmt"

	func main() {
		fmt.Println("Hello, 世界")
	}

Method pruning also helps when building small binaries with
"-ldflags=-s -w". The above program goes from 1.43MB to 1.2MB.

Unfortunately the linker can only tell if reflect.Value.Call has been
statically linked, not if it is dynamically used. And while use is
rare, it is linked into a very common standard library package,
text/template. The result is programs like cmd/go, which don't use
reflect.Value.Call, see limited benefit from this CL. If binary size
is important enough it may be possible to address this in future work.

For #6853.

Change-Id: Iabe90e210e813b08c3f8fd605f841f0458973396
Reviewed-on: https://go-review.googlesource.com/20483Reviewed-by: default avatarRuss Cox <rsc@golang.org>
parent f02dc513
......@@ -530,12 +530,12 @@ func dextratypeData(s *Sym, ot int, t *Type) int {
ot = dsymptr(s, ot, dtypesym(a.mtype), 0)
ot = dsymptr(s, ot, dtypesym(a.type_), 0)
if a.isym != nil {
ot = dsymptr(s, ot, a.isym, 0)
ot = dmethodptr(s, ot, a.isym)
} else {
ot = duintptr(s, ot, 0)
}
if a.tsym != nil {
ot = dsymptr(s, ot, a.tsym, 0)
ot = dmethodptr(s, ot, a.tsym)
} else {
ot = duintptr(s, ot, 0)
}
......@@ -543,6 +543,16 @@ func dextratypeData(s *Sym, ot int, t *Type) int {
return ot
}
func dmethodptr(s *Sym, off int, x *Sym) int {
duintptr(s, off, 0)
r := obj.Addrel(Linksym(s))
r.Off = int32(off)
r.Siz = uint8(Widthptr)
r.Sym = Linksym(x)
r.Type = obj.R_METHOD
return off + Widthptr
}
var kinds = []int{
TINT: obj.KindInt,
TUINT: obj.KindUint,
......
......@@ -469,6 +469,11 @@ const (
// should be linked into the final binary, even if there are no other
// direct references. (This is used for types reachable by reflection.)
R_USETYPE
// R_METHOD resolves to an *rtype for a method.
// It is used when linking from the uncommonType of another *rtype, and
// may be set to zero by the linker if it determines the method text is
// unreachable by the linked program.
R_METHOD
R_POWER_TOC
R_GOTPCREL
// R_JMPMIPS (only used on mips64) resolves to non-PC-relative target address
......
// Copyright 2016 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 ld
import (
"cmd/internal/obj"
"fmt"
"strings"
"unicode"
)
// deadcode marks all reachable symbols.
//
// The basis of the dead code elimination is a flood fill of symbols,
// following their relocations, begining at INITENTRY.
//
// This flood fill is wrapped in logic for pruning unused methods.
// All methods are mentioned by relocations on their receiver's *rtype.
// These relocations are specially defined as R_METHOD by the compiler
// so we can detect and manipulated them here.
//
// There are three ways a method of a reachable type can be invoked:
//
// 1. direct call
// 2. through a reachable interface type
// 3. reflect.Value.Call
//
// The first case is handled by the flood fill, a directly called method
// is marked as reachable.
//
// The second case is handled by decomposing all reachable interface
// types into method signatures. Each encountered method is compared
// against the interface method signatures, if it matches it is marked
// as reachable. This is extremely conservative, but easy and correct.
//
// The third case is handled by looking to see if reflect.Value.Call is
// ever marked reachable. If it is, all bets are off and all exported
// methods of reachable types are marked reachable.
//
// Any unreached text symbols are removed from ctxt.Textp.
func deadcode(ctxt *Link) {
if Debug['v'] != 0 {
fmt.Fprintf(ctxt.Bso, "%5.2f deadcode\n", obj.Cputime())
}
d := &deadcodepass{
ctxt: ctxt,
ifaceMethod: make(map[methodsig]bool),
}
// First, flood fill any symbols directly reachable in the call
// graph from INITENTRY. Ignore all methods not directly called.
d.init()
d.flood()
callSym := Linkrlookup(ctxt, "reflect.Value.Call", 0)
callSymSeen := false
for {
if callSym != nil && callSym.Attr.Reachable() {
// Methods are called via reflection. Give up on
// static analysis, mark all exported methods of
// all reachable types as reachable.
callSymSeen = true
}
// Mark all methods that could satisfy a discovered
// interface as reachable. We recheck old marked interfaces
// as new types (with new methods) may have been discovered
// in the last pass.
var rem []methodref
for _, m := range d.markableMethods {
if (callSymSeen && m.isExported()) || d.ifaceMethod[m.m] {
d.markMethod(m)
} else {
rem = append(rem, m)
}
}
d.markableMethods = rem
if len(d.markQueue) == 0 {
// No new work was discovered. Done.
break
}
d.flood()
}
// Remove all remaining unreached R_METHOD relocations.
for _, m := range d.markableMethods {
d.cleanupReloc(m.r0)
d.cleanupReloc(m.r1)
}
if Buildmode != BuildmodeShared {
// Keep a typelink if the symbol it points at is being kept.
// (When BuildmodeShared, always keep typelinks.)
for _, s := range ctxt.Allsym {
if strings.HasPrefix(s.Name, "go.typelink.") {
s.Attr.Set(AttrReachable, len(s.R) == 1 && s.R[0].Sym.Attr.Reachable())
}
}
}
// Remove dead text but keep file information (z symbols).
var last *LSym
for s := ctxt.Textp; s != nil; s = s.Next {
if !s.Attr.Reachable() {
continue
}
if last == nil {
ctxt.Textp = s
} else {
last.Next = s
}
last = s
}
if last == nil {
ctxt.Textp = nil
ctxt.Etextp = nil
} else {
last.Next = nil
ctxt.Etextp = last
}
}
var markextra = []string{
"runtime.morestack",
"runtime.morestackx",
"runtime.morestack00",
"runtime.morestack10",
"runtime.morestack01",
"runtime.morestack11",
"runtime.morestack8",
"runtime.morestack16",
"runtime.morestack24",
"runtime.morestack32",
"runtime.morestack40",
"runtime.morestack48",
// on arm, lock in the div/mod helpers too
"_div",
"_divu",
"_mod",
"_modu",
}
// methodref holds the relocations from a receiver type symbol to its
// method. There are two relocations, one for the method type without
// receiver, one with receiver
type methodref struct {
m methodsig
src *LSym // receiver type symbol
r0 *Reloc
r1 *Reloc
}
func (m methodref) isExported() bool {
for _, r := range m.m {
return unicode.IsUpper(r)
}
panic("methodref has no signature")
}
// deadcodepass holds state for the deadcode flood fill.
type deadcodepass struct {
ctxt *Link
markQueue []*LSym // symbols to flood fill in next pass
ifaceMethod map[methodsig]bool // methods declared in reached interfaces
markableMethods []methodref // methods of reached types
}
func (d *deadcodepass) cleanupReloc(r *Reloc) {
if r.Sym.Attr.Reachable() {
r.Type = obj.R_ADDR
} else {
if Debug['v'] > 1 {
fmt.Fprintf(d.ctxt.Bso, "removing method %s\n", r.Sym.Name)
}
r.Sym = nil
r.Siz = 0
}
}
// mark appends a symbol to the mark queue for flood filling.
func (d *deadcodepass) mark(s, parent *LSym) {
if s == nil || s.Attr.Reachable() {
return
}
s.Attr |= AttrReachable
s.Reachparent = parent
d.markQueue = append(d.markQueue, s)
}
// markMethod marks a method as reachable and preps its R_METHOD relocations.
func (d *deadcodepass) markMethod(m methodref) {
d.mark(m.r0.Sym, m.src)
d.mark(m.r1.Sym, m.src)
m.r0.Type = obj.R_ADDR
m.r1.Type = obj.R_ADDR
}
// init marks all initial symbols as reachable.
// In a typical binary, this is INITENTRY.
func (d *deadcodepass) init() {
var names []string
if Thearch.Thechar == '5' {
// mark some functions that are only referenced after linker code editing
if d.ctxt.Goarm == 5 {
names = append(names, "_sfloat")
}
names = append(names, "runtime.read_tls_fallback")
}
if Buildmode == BuildmodeShared {
// Mark all symbols defined in this library as reachable when
// building a shared library.
for _, s := range d.ctxt.Allsym {
if s.Type != 0 && s.Type != obj.SDYNIMPORT {
d.mark(s, nil)
}
}
} else {
// In a normal binary, start at main.main and the init
// functions and mark what is reachable from there.
names = append(names, INITENTRY)
if Linkshared && Buildmode == BuildmodeExe {
names = append(names, "main.main", "main.init")
}
for _, name := range markextra {
names = append(names, name)
}
for _, s := range dynexp {
d.mark(s, nil)
}
}
for _, name := range names {
d.mark(Linkrlookup(d.ctxt, name, 0), nil)
}
}
// flood flood fills symbols reachable from the markQueue symbols.
// As it goes, it collects methodref and interface method declarations.
func (d *deadcodepass) flood() {
for len(d.markQueue) > 0 {
s := d.markQueue[0]
d.markQueue = d.markQueue[1:]
if s.Type == obj.STEXT {
if Debug['v'] > 1 {
fmt.Fprintf(d.ctxt.Bso, "marktext %s\n", s.Name)
}
for _, a := range s.Autom {
d.mark(a.Gotype, s)
}
}
if strings.HasPrefix(s.Name, "type.") && s.Name[5] != '.' {
if decodetype_kind(s)&kindMask == kindInterface {
for _, sig := range decodetype_ifacemethods(s) {
if Debug['v'] > 1 {
fmt.Fprintf(d.ctxt.Bso, "reached iface method: %s\n", sig)
}
d.ifaceMethod[sig] = true
}
}
}
var methods []methodref
for i := 0; i < len(s.R); i++ {
r := &s.R[i]
if r.Sym == nil {
continue
}
if r.Type != obj.R_METHOD {
d.mark(r.Sym, s)
continue
}
// Collect rtype pointers to methods for
// later processing in deadcode.
if len(methods) > 0 {
mref := &methods[len(methods)-1]
if mref.r1 == nil {
mref.r1 = r
continue
}
}
methods = append(methods, methodref{src: s, r0: r})
}
if len(methods) > 0 {
// Decode runtime type information for type methods
// to help work out which methods can be called
// dynamically via interfaces.
methodsigs := decodetype_methods(s)
if len(methods) != len(methodsigs) {
panic(fmt.Sprintf("%q has %d method relocations for %d methods", s.Name, len(methods), len(methodsigs)))
}
for i, m := range methodsigs {
name := string(m)
name = name[:strings.Index(name, "(")]
if !strings.HasSuffix(methods[i].r0.Sym.Name, name) {
panic(fmt.Sprintf("%q relocation for %q does not match method %q", s.Name, methods[i].r0.Sym.Name, name))
}
methods[i].m = m
}
d.markableMethods = append(d.markableMethods, methods...)
}
if s.Pcln != nil {
for i := 0; i < s.Pcln.Nfuncdata; i++ {
d.mark(s.Pcln.Funcdata[i], s)
}
}
d.mark(s.Gotype, s)
d.mark(s.Sub, s)
d.mark(s.Outer, s)
}
}
......@@ -5,8 +5,10 @@
package ld
import (
"bytes"
"cmd/internal/obj"
"debug/elf"
"fmt"
)
// Decoding the type.* symbols. This has to be in sync with
......@@ -213,10 +215,9 @@ func decodetype_structfieldarrayoff(s *LSym, i int) int {
return off
}
func decodetype_structfieldname(s *LSym, i int) string {
off := decodetype_structfieldarrayoff(s, i)
func decodetype_stringptr(s *LSym, off int) string {
s = decode_reloc_sym(s, int32(off))
if s == nil { // embedded structs have a nil name.
if s == nil {
return ""
}
r := decode_reloc(s, 0) // s has a pointer to the string data at offset 0
......@@ -227,6 +228,11 @@ func decodetype_structfieldname(s *LSym, i int) string {
return string(r.Sym.P[r.Add : r.Add+strlen])
}
func decodetype_structfieldname(s *LSym, i int) string {
off := decodetype_structfieldarrayoff(s, i)
return decodetype_stringptr(s, off)
}
func decodetype_structfieldtype(s *LSym, i int) *LSym {
off := decodetype_structfieldarrayoff(s, i)
return decode_reloc_sym(s, int32(off+2*Thearch.Ptrsize))
......@@ -241,3 +247,110 @@ func decodetype_structfieldoffs(s *LSym, i int) int64 {
func decodetype_ifacemethodcount(s *LSym) int64 {
return int64(decode_inuxi(s.P[commonsize()+Thearch.Ptrsize:], Thearch.Intsize))
}
// methodsig is a fully qualified typed method signature, like
// "Visit(type.go/ast.Node) (type.go/ast.Visitor)".
type methodsig string
// Matches runtime/typekind.go and reflect.Kind.
const (
kindArray = 17
kindChan = 18
kindFunc = 19
kindInterface = 20
kindMap = 21
kindPtr = 22
kindSlice = 23
kindStruct = 25
kindMask = (1 << 5) - 1
)
// decode_methodsig decodes an array of method signature information.
// Each element of the array is size bytes. The first word is a *string
// for the name, the third word is a *rtype for the funcType.
//
// Conveniently this is the layout of both runtime.method and runtime.imethod.
func decode_methodsig(s *LSym, off, size, count int) []methodsig {
var buf bytes.Buffer
var methods []methodsig
for i := 0; i < count; i++ {
buf.WriteString(decodetype_stringptr(s, off))
mtypSym := decode_reloc_sym(s, int32(off+2*Thearch.Ptrsize))
buf.WriteRune('(')
inCount := decodetype_funcincount(mtypSym)
for i := 0; i < inCount; i++ {
if i > 0 {
buf.WriteString(", ")
}
buf.WriteString(decodetype_funcintype(mtypSym, i).Name)
}
buf.WriteString(") (")
outCount := decodetype_funcoutcount(mtypSym)
for i := 0; i < outCount; i++ {
if i > 0 {
buf.WriteString(", ")
}
buf.WriteString(decodetype_funcouttype(mtypSym, i).Name)
}
buf.WriteRune(')')
off += size
methods = append(methods, methodsig(buf.String()))
buf.Reset()
}
return methods
}
func decodetype_ifacemethods(s *LSym) []methodsig {
if decodetype_kind(s)&kindMask != kindInterface {
panic(fmt.Sprintf("symbol %q is not an interface", s.Name))
}
r := decode_reloc(s, int32(commonsize()))
if r == nil {
return nil
}
if r.Sym != s {
panic(fmt.Sprintf("imethod slice pointer in %q leads to a different symbol", s.Name))
}
off := int(r.Add) // array of reflect.imethod values
numMethods := int(decodetype_ifacemethodcount(s))
sizeofIMethod := 3 * Thearch.Ptrsize
return decode_methodsig(s, off, sizeofIMethod, numMethods)
}
func decodetype_methods(s *LSym) []methodsig {
if !decodetype_hasUncommon(s) {
panic(fmt.Sprintf("no methods on %q", s.Name))
}
off := commonsize() // reflect.rtype
switch decodetype_kind(s) & kindMask {
case kindStruct: // reflect.structType
off += Thearch.Ptrsize + 2*Thearch.Intsize
case kindPtr: // reflect.ptrType
off += Thearch.Ptrsize
case kindFunc: // reflect.funcType
off += Thearch.Ptrsize // 4 bytes, pointer aligned
case kindSlice: // reflect.sliceType
off += Thearch.Ptrsize
case kindArray: // reflect.arrayType
off += 3 * Thearch.Ptrsize
case kindChan: // reflect.chanType
off += 2 * Thearch.Ptrsize
case kindMap: // reflect.mapType
off += 4*Thearch.Ptrsize + 8
case kindInterface: // reflect.interfaceType
off += Thearch.Ptrsize + 2*Thearch.Intsize
default:
// just Sizeof(rtype)
}
numMethods := int(decode_inuxi(s.P[off+2*Thearch.Ptrsize:], Thearch.Intsize))
r := decode_reloc(s, int32(off+Thearch.Ptrsize))
if r.Sym != s {
panic(fmt.Sprintf("method slice pointer in %q leads to a different symbol", s.Name))
}
off = int(r.Add) // array of reflect.method values
sizeofMethod := 6 * Thearch.Ptrsize
return decode_methodsig(s, off, sizeofMethod, numMethods)
}
......@@ -367,133 +367,10 @@ func Adddynsym(ctxt *Link, s *LSym) {
}
}
var markQueue []*LSym
func mark1(s *LSym, parent *LSym) {
if s == nil || s.Attr.Reachable() {
return
}
s.Attr |= AttrReachable
s.Reachparent = parent
markQueue = append(markQueue, s)
}
func mark(s *LSym) {
mark1(s, nil)
}
// markflood makes the dependencies of any reachable symable also reachable.
func markflood() {
for len(markQueue) > 0 {
s := markQueue[0]
markQueue = markQueue[1:]
if s.Type == obj.STEXT {
if Debug['v'] > 1 {
fmt.Fprintf(&Bso, "marktext %s\n", s.Name)
}
for _, a := range s.Autom {
mark1(a.Gotype, s)
}
}
for i := 0; i < len(s.R); i++ {
mark1(s.R[i].Sym, s)
}
if s.Pcln != nil {
for i := 0; i < s.Pcln.Nfuncdata; i++ {
mark1(s.Pcln.Funcdata[i], s)
}
}
mark1(s.Gotype, s)
mark1(s.Sub, s)
mark1(s.Outer, s)
}
}
var markextra = []string{
"runtime.morestack",
"runtime.morestackx",
"runtime.morestack00",
"runtime.morestack10",
"runtime.morestack01",
"runtime.morestack11",
"runtime.morestack8",
"runtime.morestack16",
"runtime.morestack24",
"runtime.morestack32",
"runtime.morestack40",
"runtime.morestack48",
// on arm, lock in the div/mod helpers too
"_div",
"_divu",
"_mod",
"_modu",
}
func deadcode() {
if Debug['v'] != 0 {
fmt.Fprintf(&Bso, "%5.2f deadcode\n", obj.Cputime())
}
if Buildmode == BuildmodeShared {
// Mark all symbols defined in this library as reachable when
// building a shared library.
for _, s := range Ctxt.Allsym {
if s.Type != 0 && s.Type != obj.SDYNIMPORT {
mark(s)
}
}
markflood()
} else {
mark(Linklookup(Ctxt, INITENTRY, 0))
if Linkshared && Buildmode == BuildmodeExe {
mark(Linkrlookup(Ctxt, "main.main", 0))
mark(Linkrlookup(Ctxt, "main.init", 0))
}
for i := 0; i < len(markextra); i++ {
mark(Linklookup(Ctxt, markextra[i], 0))
}
for i := 0; i < len(dynexp); i++ {
mark(dynexp[i])
}
markflood()
// keep each beginning with 'typelink.' if the symbol it points at is being kept.
for _, s := range Ctxt.Allsym {
if strings.HasPrefix(s.Name, "go.typelink.") {
s.Attr.Set(AttrReachable, len(s.R) == 1 && s.R[0].Sym.Attr.Reachable())
}
}
// remove dead text but keep file information (z symbols).
var last *LSym
for s := Ctxt.Textp; s != nil; s = s.Next {
if !s.Attr.Reachable() {
continue
}
// NOTE: Removing s from old textp and adding to new, shorter textp.
if last == nil {
Ctxt.Textp = s
} else {
last.Next = s
}
last = s
}
if last == nil {
Ctxt.Textp = nil
Ctxt.Etextp = nil
} else {
last.Next = nil
Ctxt.Etextp = last
}
}
func fieldtrack(ctxt *Link) {
// record field tracking references
var buf bytes.Buffer
for _, s := range Ctxt.Allsym {
for _, s := range ctxt.Allsym {
if strings.HasPrefix(s.Name, "go.track.") {
s.Attr |= AttrSpecial // do not lay out in data segment
s.Attr |= AttrHidden
......@@ -514,7 +391,7 @@ func deadcode() {
if tracksym == "" {
return
}
s := Linklookup(Ctxt, tracksym, 0)
s := Linklookup(ctxt, tracksym, 0)
if !s.Attr.Reachable() {
return
}
......
......@@ -186,16 +186,9 @@ func Ldmain() {
}
loadlib()
if Thearch.Thechar == '5' {
// mark some functions that are only referenced after linker code editing
if Ctxt.Goarm == 5 {
mark(Linkrlookup(Ctxt, "_sfloat", 0))
}
mark(Linklookup(Ctxt, "runtime.read_tls_fallback", 0))
}
checkstrdata()
deadcode()
deadcode(Ctxt)
fieldtrack(Ctxt)
callgraph()
doelf()
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment