Skip to content

Commit 295307a

Browse files
philhoferdr2chase
authored andcommitted
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 #19361 Change-Id: I57d4dc21ef40a05ec0fbd55a9bb0eb74cdc67a3d Reviewed-on: https://go-review.googlesource.com/38139 Run-TryBot: Philip Hofer <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: David Chase <[email protected]>
1 parent 6917553 commit 295307a

File tree

10 files changed

+234
-17
lines changed

10 files changed

+234
-17
lines changed

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

+4
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,11 @@ func Main() {
483483
}
484484
}
485485

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

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

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

+81-1
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)
@@ -1379,6 +1387,78 @@ ok:
13791387
return s
13801388
}
13811389

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

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

+4
Original file line numberDiff line numberDiff line change
@@ -4967,6 +4967,10 @@ func (e *ssaExport) SplitArray(name ssa.LocalSlot) ssa.LocalSlot {
49674967
return ssa.LocalSlot{N: n, Type: et, Off: name.Off}
49684968
}
49694969

4970+
func (e *ssaExport) DerefItab(it *obj.LSym, offset int64) *obj.LSym {
4971+
return itabsym(it, offset)
4972+
}
4973+
49704974
// namedAuto returns a new AUTO variable with the given name and type.
49714975
// These are exposed to the debugger.
49724976
func (e *ssaExport) namedAuto(name string, typ ssa.Type) ssa.GCNode {

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

+9-1
Original file line numberDiff line numberDiff line change
@@ -1679,7 +1679,6 @@ func structargs(tl *Type, mustname bool) []*Node {
16791679
// rcvr - U
16801680
// method - M func (t T)(), a TFIELD type struct
16811681
// newnam - the eventual mangled name of this function
1682-
16831682
func genwrapper(rcvr *Type, method *Field, newnam *Sym, iface int) {
16841683
if false && Debug['r'] != 0 {
16851684
fmt.Printf("genwrapper rcvrtype=%v method=%v newnam=%v\n", rcvr, method, newnam)
@@ -1715,6 +1714,7 @@ func genwrapper(rcvr *Type, method *Field, newnam *Sym, iface int) {
17151714
fn.Func.Nname = newname(newnam)
17161715
fn.Func.Nname.Name.Defn = fn
17171716
fn.Func.Nname.Name.Param.Ntype = t
1717+
fn.Func.Nname.Sym.SetExported(true) // prevent export; see closure.go
17181718
declare(fn.Func.Nname, PFUNC)
17191719
funchdr(fn)
17201720

@@ -1918,6 +1918,14 @@ func implements(t, iface *Type, m, samename **Field, ptr *int) bool {
19181918
}
19191919
}
19201920

1921+
// We're going to emit an OCONVIFACE.
1922+
// Call itabname so that (t, iface)
1923+
// gets added to itabs early, which allows
1924+
// us to de-virtualize calls through this
1925+
// type/interface pair later. See peekitabs in reflect.go
1926+
if isdirectiface(t0) && !iface.IsEmptyInterface() {
1927+
itabname(t0, iface)
1928+
}
19211929
return true
19221930
}
19231931

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

+6
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

+16-15
Original file line numberDiff line numberDiff line change
@@ -97,21 +97,22 @@ func (d DummyFrontend) Warnl(_ src.XPos, msg string, args ...interface{}) { d.t
9797
func (d DummyFrontend) Debug_checknil() bool { return false }
9898
func (d DummyFrontend) Debug_wb() bool { return false }
9999

100-
func (d DummyFrontend) TypeBool() Type { return TypeBool }
101-
func (d DummyFrontend) TypeInt8() Type { return TypeInt8 }
102-
func (d DummyFrontend) TypeInt16() Type { return TypeInt16 }
103-
func (d DummyFrontend) TypeInt32() Type { return TypeInt32 }
104-
func (d DummyFrontend) TypeInt64() Type { return TypeInt64 }
105-
func (d DummyFrontend) TypeUInt8() Type { return TypeUInt8 }
106-
func (d DummyFrontend) TypeUInt16() Type { return TypeUInt16 }
107-
func (d DummyFrontend) TypeUInt32() Type { return TypeUInt32 }
108-
func (d DummyFrontend) TypeUInt64() Type { return TypeUInt64 }
109-
func (d DummyFrontend) TypeFloat32() Type { return TypeFloat32 }
110-
func (d DummyFrontend) TypeFloat64() Type { return TypeFloat64 }
111-
func (d DummyFrontend) TypeInt() Type { return TypeInt64 }
112-
func (d DummyFrontend) TypeUintptr() Type { return TypeUInt64 }
113-
func (d DummyFrontend) TypeString() Type { panic("unimplemented") }
114-
func (d DummyFrontend) TypeBytePtr() Type { return TypeBytePtr }
100+
func (d DummyFrontend) TypeBool() Type { return TypeBool }
101+
func (d DummyFrontend) TypeInt8() Type { return TypeInt8 }
102+
func (d DummyFrontend) TypeInt16() Type { return TypeInt16 }
103+
func (d DummyFrontend) TypeInt32() Type { return TypeInt32 }
104+
func (d DummyFrontend) TypeInt64() Type { return TypeInt64 }
105+
func (d DummyFrontend) TypeUInt8() Type { return TypeUInt8 }
106+
func (d DummyFrontend) TypeUInt16() Type { return TypeUInt16 }
107+
func (d DummyFrontend) TypeUInt32() Type { return TypeUInt32 }
108+
func (d DummyFrontend) TypeUInt64() Type { return TypeUInt64 }
109+
func (d DummyFrontend) TypeFloat32() Type { return TypeFloat32 }
110+
func (d DummyFrontend) TypeFloat64() Type { return TypeFloat64 }
111+
func (d DummyFrontend) TypeInt() Type { return TypeInt64 }
112+
func (d DummyFrontend) TypeUintptr() Type { return TypeUInt64 }
113+
func (d DummyFrontend) TypeString() Type { panic("unimplemented") }
114+
func (d DummyFrontend) TypeBytePtr() Type { return TypeBytePtr }
115+
func (d DummyFrontend) DerefItab(sym *obj.LSym, off int64) *obj.LSym { return nil }
115116

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

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

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

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

+20
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

+48
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:
@@ -5736,6 +5738,52 @@ func rewriteValuegeneric_OpIMake(v *Value, config *Config) bool {
57365738
}
57375739
return false
57385740
}
5741+
func rewriteValuegeneric_OpInterCall(v *Value, config *Config) bool {
5742+
b := v.Block
5743+
_ = b
5744+
// match: (InterCall [argsize] (Load (OffPtr [off] (ITab (IMake (Addr {itab} (SB)) _))) _) mem)
5745+
// cond: devirt(v, itab, off) != nil
5746+
// result: (StaticCall [argsize] {devirt(v, itab, off)} mem)
5747+
for {
5748+
argsize := v.AuxInt
5749+
v_0 := v.Args[0]
5750+
if v_0.Op != OpLoad {
5751+
break
5752+
}
5753+
v_0_0 := v_0.Args[0]
5754+
if v_0_0.Op != OpOffPtr {
5755+
break
5756+
}
5757+
off := v_0_0.AuxInt
5758+
v_0_0_0 := v_0_0.Args[0]
5759+
if v_0_0_0.Op != OpITab {
5760+
break
5761+
}
5762+
v_0_0_0_0 := v_0_0_0.Args[0]
5763+
if v_0_0_0_0.Op != OpIMake {
5764+
break
5765+
}
5766+
v_0_0_0_0_0 := v_0_0_0_0.Args[0]
5767+
if v_0_0_0_0_0.Op != OpAddr {
5768+
break
5769+
}
5770+
itab := v_0_0_0_0_0.Aux
5771+
v_0_0_0_0_0_0 := v_0_0_0_0_0.Args[0]
5772+
if v_0_0_0_0_0_0.Op != OpSB {
5773+
break
5774+
}
5775+
mem := v.Args[1]
5776+
if !(devirt(v, itab, off) != nil) {
5777+
break
5778+
}
5779+
v.reset(OpStaticCall)
5780+
v.AuxInt = argsize
5781+
v.Aux = devirt(v, itab, off)
5782+
v.AddArg(mem)
5783+
return true
5784+
}
5785+
return false
5786+
}
57395787
func rewriteValuegeneric_OpIsInBounds(v *Value, config *Config) bool {
57405788
b := v.Block
57415789
_ = b

test/devirt.go

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// errorcheck -0 -d=ssa/opt/debug=3
2+
3+
package main
4+
5+
// Trivial interface call devirtualization test.
6+
7+
type real struct {
8+
value int
9+
}
10+
11+
func (r *real) Value() int { return r.value }
12+
13+
type Valuer interface {
14+
Value() int
15+
}
16+
17+
type indirectiface struct {
18+
a, b, c int
19+
}
20+
21+
func (i indirectiface) Value() int {
22+
return i.a + i.b + i.c
23+
}
24+
25+
func main() {
26+
var r Valuer
27+
rptr := &real{value: 3}
28+
r = rptr
29+
30+
if r.Value() != 3 { // ERROR "de-virtualizing call$"
31+
panic("not 3")
32+
}
33+
34+
// Can't do types that aren't "direct" interfaces (yet).
35+
r = indirectiface{3, 4, 5}
36+
if r.Value() != 12 {
37+
panic("not 12")
38+
}
39+
}

0 commit comments

Comments
 (0)