Skip to content

Commit f3d1a1c

Browse files
committed
cmd/compile: de-virtualize interface calls
With this change, code like h := sha1.New() h.Write(buf) sum := h.Sum() gets compiled into static calls rather than interface calls, because the compiler is able to prove that 'h' is really a *sha1.digest. The InterCall re-write rule hits a few dozen times during make.bash, and hundreds of times during all.bash. The most common pattern identified by the compiler is a constructor like func New() Interface { return &impl{...} } where the constructor gets inlined into the caller, and the result is used immediately. Examples include {sha1,md5,crc32,crc64,...}.New, base64.NewEncoder, base64.NewDecoder, errors.New, net.Pipe, and so on. Some existing benchmarks that change on darwin/amd64: Crc64/ISO4KB-8 2.67µs ± 1% 2.66µs ± 0% -0.36% (p=0.015 n=10+10) Crc64/ISO1KB-8 694ns ± 0% 690ns ± 1% -0.59% (p=0.001 n=10+10) Adler32KB-8 473ns ± 1% 471ns ± 0% -0.39% (p=0.010 n=10+9) On architectures like amd64, the reduction in code size appears to contribute more to benchmark improvements than just removing the indirect call, since that branch gets predicted accurately when called in a loop. Updates golang#19361 Change-Id: Ia9d30afdd5f6b4d38d38b14b88f308acae8ce7ed
1 parent 38409f5 commit f3d1a1c

File tree

12 files changed

+276
-19
lines changed

12 files changed

+276
-19
lines changed

src/cmd/compile/internal/gc/main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,11 @@ func Main() {
482482
}
483483
}
484484

485+
// Just before compilation, compile itabs found on
486+
// the right side of OCONVIFACE so that methods
487+
// can be de-virtualized during compilation.
485488
Curfn = nil
489+
peekitabs()
486490

487491
// Phase 8: Compile top level functions.
488492
// Don't use range--walk can add functions to xtop.

src/cmd/compile/internal/gc/reflect.go

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ import (
1616
type itabEntry struct {
1717
t, itype *Type
1818
sym *Sym
19+
20+
// symbol of the itab itself;
21+
// filled in lazily after typecheck
22+
lsym *obj.LSym
23+
24+
// symbols of each method in
25+
// the itab, sorted by byte offset;
26+
// filled in at the same time as lsym
27+
entries []*obj.LSym
1928
}
2029

2130
type ptabEntry struct {
@@ -415,7 +424,6 @@ func imethods(t *Type) []*Sig {
415424
// Generate the method body, so that compiled
416425
// code can refer to it.
417426
isym := methodsym(method, t, 0)
418-
419427
if !isym.Siggen() {
420428
isym.SetSiggen(true)
421429
genwrapper(t, f, isym, 0)
@@ -1004,7 +1012,11 @@ func itabname(t, itype *Type) *Node {
10041012
n.Typecheck = 1
10051013
s.Def = n
10061014

1007-
itabs = append(itabs, itabEntry{t: t, itype: itype, sym: s})
1015+
itabs = append(itabs, itabEntry{
1016+
t: t,
1017+
itype: itype,
1018+
sym: s,
1019+
})
10081020
}
10091021

10101022
n := nod(OADDR, s.Def, nil)
@@ -1379,6 +1391,78 @@ ok:
13791391
return s
13801392
}
13811393

1394+
// for each itabEntry, gather the methods on
1395+
// the concrete type that implement the interface
1396+
func peekitabs() {
1397+
for i := range itabs {
1398+
tab := &itabs[i]
1399+
methods := genfun(tab.t, tab.itype)
1400+
if len(methods) == 0 {
1401+
continue
1402+
}
1403+
tab.lsym = Linksym(tab.sym)
1404+
tab.entries = methods
1405+
}
1406+
}
1407+
1408+
// for the given concrete type and interface
1409+
// type, return the (sorted) set of methods
1410+
// on the concrete type that implement the interface
1411+
func genfun(t, it *Type) []*obj.LSym {
1412+
if t == nil || it == nil {
1413+
return nil
1414+
}
1415+
sigs := imethods(it)
1416+
methods := methods(t)
1417+
out := make([]*obj.LSym, 0, len(sigs))
1418+
if len(sigs) == 0 {
1419+
return nil
1420+
}
1421+
1422+
// both sigs and methods are sorted by name,
1423+
// so we can find the intersect in a single pass
1424+
for _, m := range methods {
1425+
if m.name == sigs[0].name {
1426+
out = append(out, Linksym(m.isym))
1427+
sigs = sigs[1:]
1428+
if len(sigs) == 0 {
1429+
break
1430+
}
1431+
}
1432+
}
1433+
1434+
return out
1435+
}
1436+
1437+
// itabsym uses the information gathered in
1438+
// peekitabs to de-virtualize interface methods.
1439+
// Since this is called by the SSA backend, it shouldn't
1440+
// generate additional Nodes, Syms, etc.
1441+
func itabsym(it *obj.LSym, offset int64) *obj.LSym {
1442+
var syms []*obj.LSym
1443+
if it == nil {
1444+
return nil
1445+
}
1446+
1447+
for i := range itabs {
1448+
e := &itabs[i]
1449+
if e.lsym == it {
1450+
syms = e.entries
1451+
break
1452+
}
1453+
}
1454+
if syms == nil {
1455+
return nil
1456+
}
1457+
1458+
// keep this arithmetic in sync with *itab layout
1459+
methodnum := int((offset - 3*int64(Widthptr) - 8) / int64(Widthptr))
1460+
if methodnum >= len(syms) {
1461+
return nil
1462+
}
1463+
return syms[methodnum]
1464+
}
1465+
13821466
func dumptypestructs() {
13831467
// copy types from externdcl list to signatlist
13841468
for _, n := range externdcl {

src/cmd/compile/internal/gc/ssa.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4914,6 +4914,10 @@ func (e *ssaExport) SplitArray(name ssa.LocalSlot) ssa.LocalSlot {
49144914
return ssa.LocalSlot{N: n, Type: et, Off: name.Off}
49154915
}
49164916

4917+
func (e *ssaExport) DerefItab(it *obj.LSym, offset int64) *obj.LSym {
4918+
return itabsym(it, offset)
4919+
}
4920+
49174921
// namedAuto returns a new AUTO variable with the given name and type.
49184922
// These are exposed to the debugger.
49194923
func (e *ssaExport) namedAuto(name string, typ ssa.Type) ssa.GCNode {

src/cmd/compile/internal/gc/subr.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1684,7 +1684,6 @@ func structargs(tl *Type, mustname bool) []*Node {
16841684
// rcvr - U
16851685
// method - M func (t T)(), a TFIELD type struct
16861686
// newnam - the eventual mangled name of this function
1687-
16881687
func genwrapper(rcvr *Type, method *Field, newnam *Sym, iface int) {
16891688
if false && Debug['r'] != 0 {
16901689
fmt.Printf("genwrapper rcvrtype=%v method=%v newnam=%v\n", rcvr, method, newnam)
@@ -1720,6 +1719,7 @@ func genwrapper(rcvr *Type, method *Field, newnam *Sym, iface int) {
17201719
fn.Func.Nname = newname(newnam)
17211720
fn.Func.Nname.Name.Defn = fn
17221721
fn.Func.Nname.Name.Param.Ntype = t
1722+
fn.Func.Nname.Sym.SetExported(true) // prevent export; see closure.go
17231723
declare(fn.Func.Nname, PFUNC)
17241724
funchdr(fn)
17251725

@@ -1923,6 +1923,14 @@ func implements(t, iface *Type, m, samename **Field, ptr *int) bool {
19231923
}
19241924
}
19251925

1926+
// We're going to emit an OCONVIFACE.
1927+
// Call itabname so that (t, iface)
1928+
// gets added to itabs early, which allows
1929+
// us to de-virtualize calls through this
1930+
// type/interface pair later. See peekitabs in reflect.go
1931+
if isdirectiface(t0) && !iface.IsEmptyInterface() {
1932+
itabname(t0, iface)
1933+
}
19261934
return true
19271935
}
19281936

src/cmd/compile/internal/ssa/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,12 @@ type Frontend interface {
121121
SplitArray(LocalSlot) LocalSlot // array must be length 1
122122
SplitInt64(LocalSlot) (LocalSlot, LocalSlot) // returns (hi, lo)
123123

124+
// DerefItab dereferences an itab function
125+
// entry, given the symbol of the itab and
126+
// the byte offset of the function pointer.
127+
// It may return nil.
128+
DerefItab(sym *obj.LSym, offset int64) *obj.LSym
129+
124130
// Line returns a string describing the given position.
125131
Line(src.XPos) string
126132

src/cmd/compile/internal/ssa/export_test.go

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -84,21 +84,22 @@ func (d DummyFrontend) Warnl(_ src.XPos, msg string, args ...interface{}) { d.t
8484
func (d DummyFrontend) Debug_checknil() bool { return false }
8585
func (d DummyFrontend) Debug_wb() bool { return false }
8686

87-
func (d DummyFrontend) TypeBool() Type { return TypeBool }
88-
func (d DummyFrontend) TypeInt8() Type { return TypeInt8 }
89-
func (d DummyFrontend) TypeInt16() Type { return TypeInt16 }
90-
func (d DummyFrontend) TypeInt32() Type { return TypeInt32 }
91-
func (d DummyFrontend) TypeInt64() Type { return TypeInt64 }
92-
func (d DummyFrontend) TypeUInt8() Type { return TypeUInt8 }
93-
func (d DummyFrontend) TypeUInt16() Type { return TypeUInt16 }
94-
func (d DummyFrontend) TypeUInt32() Type { return TypeUInt32 }
95-
func (d DummyFrontend) TypeUInt64() Type { return TypeUInt64 }
96-
func (d DummyFrontend) TypeFloat32() Type { return TypeFloat32 }
97-
func (d DummyFrontend) TypeFloat64() Type { return TypeFloat64 }
98-
func (d DummyFrontend) TypeInt() Type { return TypeInt64 }
99-
func (d DummyFrontend) TypeUintptr() Type { return TypeUInt64 }
100-
func (d DummyFrontend) TypeString() Type { panic("unimplemented") }
101-
func (d DummyFrontend) TypeBytePtr() Type { return TypeBytePtr }
87+
func (d DummyFrontend) TypeBool() Type { return TypeBool }
88+
func (d DummyFrontend) TypeInt8() Type { return TypeInt8 }
89+
func (d DummyFrontend) TypeInt16() Type { return TypeInt16 }
90+
func (d DummyFrontend) TypeInt32() Type { return TypeInt32 }
91+
func (d DummyFrontend) TypeInt64() Type { return TypeInt64 }
92+
func (d DummyFrontend) TypeUInt8() Type { return TypeUInt8 }
93+
func (d DummyFrontend) TypeUInt16() Type { return TypeUInt16 }
94+
func (d DummyFrontend) TypeUInt32() Type { return TypeUInt32 }
95+
func (d DummyFrontend) TypeUInt64() Type { return TypeUInt64 }
96+
func (d DummyFrontend) TypeFloat32() Type { return TypeFloat32 }
97+
func (d DummyFrontend) TypeFloat64() Type { return TypeFloat64 }
98+
func (d DummyFrontend) TypeInt() Type { return TypeInt64 }
99+
func (d DummyFrontend) TypeUintptr() Type { return TypeUInt64 }
100+
func (d DummyFrontend) TypeString() Type { panic("unimplemented") }
101+
func (d DummyFrontend) TypeBytePtr() Type { return TypeBytePtr }
102+
func (d DummyFrontend) DerefItab(sym *obj.LSym, off int64) *obj.LSym { return nil }
102103

103104
func (d DummyFrontend) CanSSA(t Type) bool {
104105
// There are no un-SSAable types in dummy land.

src/cmd/compile/internal/ssa/gen/generic.rules

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1430,3 +1430,10 @@
14301430
&& c == config.ctxt.FixedFrameSize() + config.RegSize // offset of return value
14311431
&& warnRule(config.Debug_checknil() && v.Pos.Line() > 1, v, "removed nil check")
14321432
-> (Invalid)
1433+
1434+
// De-virtualize interface calls into static calls.
1435+
// Note that (ITab (IMake)) doesn't get
1436+
// rewritten until after the first opt pass,
1437+
// so this rule should trigger reliably.
1438+
(InterCall [argsize] (Load (OffPtr [off] (ITab (IMake (Addr {itab} (SB)) _))) _) mem) && devirt(v, itab, off) != nil ->
1439+
(StaticCall [argsize] {devirt(v, itab, off)} mem)

src/cmd/compile/internal/ssa/rewrite.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package ssa
66

77
import (
8+
"cmd/internal/obj"
89
"crypto/sha1"
910
"fmt"
1011
"math"
@@ -384,6 +385,25 @@ func uaddOvf(a, b int64) bool {
384385
return uint64(a)+uint64(b) < uint64(a)
385386
}
386387

388+
// de-virtualize an InterCall
389+
// 'sym' is the symbol for the itab
390+
func devirt(v *Value, sym interface{}, offset int64) *obj.LSym {
391+
f := v.Block.Func
392+
ext, ok := sym.(*ExternSymbol)
393+
if !ok {
394+
return nil
395+
}
396+
lsym := f.Config.Frontend().DerefItab(ext.Sym, offset)
397+
if f.pass.debug > 0 {
398+
if lsym != nil {
399+
f.Config.Warnl(v.Pos, "de-virtualizing call")
400+
} else {
401+
f.Config.Warnl(v.Pos, "couldn't de-virtualize call")
402+
}
403+
}
404+
return lsym
405+
}
406+
387407
// isSamePtr reports whether p1 and p2 point to the same address.
388408
func isSamePtr(p1, p2 *Value) bool {
389409
if p1 == p2 {

src/cmd/compile/internal/ssa/rewritegeneric.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ func rewriteValuegeneric(v *Value, config *Config) bool {
124124
return rewriteValuegeneric_OpGreater8U(v, config)
125125
case OpIMake:
126126
return rewriteValuegeneric_OpIMake(v, config)
127+
case OpInterCall:
128+
return rewriteValuegeneric_OpInterCall(v, config)
127129
case OpIsInBounds:
128130
return rewriteValuegeneric_OpIsInBounds(v, config)
129131
case OpIsNonNil:
@@ -5719,6 +5721,52 @@ func rewriteValuegeneric_OpIMake(v *Value, config *Config) bool {
57195721
}
57205722
return false
57215723
}
5724+
func rewriteValuegeneric_OpInterCall(v *Value, config *Config) bool {
5725+
b := v.Block
5726+
_ = b
5727+
// match: (InterCall [argsize] (Load (OffPtr [off] (ITab (IMake (Addr {itab} (SB)) _))) _) mem)
5728+
// cond: devirt(v, itab, off) != nil
5729+
// result: (StaticCall [argsize] {devirt(v, itab, off)} mem)
5730+
for {
5731+
argsize := v.AuxInt
5732+
v_0 := v.Args[0]
5733+
if v_0.Op != OpLoad {
5734+
break
5735+
}
5736+
v_0_0 := v_0.Args[0]
5737+
if v_0_0.Op != OpOffPtr {
5738+
break
5739+
}
5740+
off := v_0_0.AuxInt
5741+
v_0_0_0 := v_0_0.Args[0]
5742+
if v_0_0_0.Op != OpITab {
5743+
break
5744+
}
5745+
v_0_0_0_0 := v_0_0_0.Args[0]
5746+
if v_0_0_0_0.Op != OpIMake {
5747+
break
5748+
}
5749+
v_0_0_0_0_0 := v_0_0_0_0.Args[0]
5750+
if v_0_0_0_0_0.Op != OpAddr {
5751+
break
5752+
}
5753+
itab := v_0_0_0_0_0.Aux
5754+
v_0_0_0_0_0_0 := v_0_0_0_0_0.Args[0]
5755+
if v_0_0_0_0_0_0.Op != OpSB {
5756+
break
5757+
}
5758+
mem := v.Args[1]
5759+
if !(devirt(v, itab, off) != nil) {
5760+
break
5761+
}
5762+
v.reset(OpStaticCall)
5763+
v.AuxInt = argsize
5764+
v.Aux = devirt(v, itab, off)
5765+
v.AddArg(mem)
5766+
return true
5767+
}
5768+
return false
5769+
}
57225770
func rewriteValuegeneric_OpIsInBounds(v *Value, config *Config) bool {
57235771
b := v.Block
57245772
_ = b

src/cmd/compile/internal/ssa/tighten.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func tighten(f *Func) {
2020
// Tuple selectors must stay with the tuple generator.
2121
continue
2222
}
23-
if len(v.Args) > 0 && v.Args[len(v.Args)-1].Type.IsMemory() {
23+
if v.MemoryArg() != nil {
2424
// We can't move values which have a memory arg - it might
2525
// make two memory values live across a block boundary.
2626
continue

src/cmd/compile/internal/ssa/value.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,3 +311,14 @@ func (v *Value) RegName() string {
311311
}
312312
return reg.(*Register).name
313313
}
314+
315+
func (v *Value) MemoryArg() *Value {
316+
na := len(v.Args)
317+
if na == 0 {
318+
return nil
319+
}
320+
if last := v.Args[na-1]; last.Type.IsMemory() {
321+
return last
322+
}
323+
return nil
324+
}

0 commit comments

Comments
 (0)