Skip to content

Commit 4e0c7c3

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: Ia9d30afdd5f6b4d38d38b14b88f308acae8ce7ed Reviewed-on: https://go-review.googlesource.com/37751 Run-TryBot: Philip Hofer <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Keith Randall <[email protected]>
1 parent 26e726c commit 4e0c7c3

File tree

10 files changed

+259
-17
lines changed

10 files changed

+259
-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
@@ -4931,6 +4931,10 @@ func (e *ssaExport) SplitArray(name ssa.LocalSlot) ssa.LocalSlot {
49314931
return ssa.LocalSlot{N: n, Type: et, Off: name.Off}
49324932
}
49334933

4934+
func (e *ssaExport) DerefItab(it *obj.LSym, offset int64) *obj.LSym {
4935+
return itabsym(it, offset)
4936+
}
4937+
49344938
// namedAuto returns a new AUTO variable with the given name and type.
49354939
// These are exposed to the debugger.
49364940
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
@@ -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

+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

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// errorcheck -0 -d=ssa/opt/debug=3
2+
3+
package main
4+
5+
import (
6+
"crypto/sha1"
7+
"errors"
8+
"fmt"
9+
"sync"
10+
)
11+
12+
func f0() {
13+
v := errors.New("error string")
14+
_ = v.Error() // ERROR "de-virtualizing call$"
15+
}
16+
17+
func f1() {
18+
h := sha1.New()
19+
buf := make([]byte, 4)
20+
h.Write(buf) // ERROR "de-virtualizing call$"
21+
_ = h.Sum(nil) // ERROR "de-virtualizing call$"
22+
}
23+
24+
func f2() {
25+
// trickier case: make sure we see this is *sync.rlocker
26+
// instead of *sync.RWMutex,
27+
// even though they are the same pointers
28+
var m sync.RWMutex
29+
r := m.RLocker()
30+
31+
// deadlock if the type of 'r' is improperly interpreted
32+
// as *sync.RWMutex
33+
r.Lock() // ERROR "de-virtualizing call$"
34+
m.RLock()
35+
r.Unlock() // ERROR "de-virtualizing call$"
36+
m.RUnlock()
37+
}
38+
39+
type multiword struct{ a, b, c int }
40+
41+
func (m multiword) Error() string { return fmt.Sprintf("%d, %d, %d", m.a, m.b, m.c) }
42+
43+
func f3() {
44+
// can't de-virtualize this one yet;
45+
// it passes through a call to iconvT2I
46+
var err error
47+
err = multiword{1, 2, 3}
48+
if err.Error() != "1, 2, 3" {
49+
panic("bad call")
50+
}
51+
52+
// ... but we can do this one
53+
err = &multiword{1, 2, 3}
54+
if err.Error() != "1, 2, 3" { // ERROR "de-virtualizing call$"
55+
panic("bad call")
56+
}
57+
}
58+
59+
func main() {
60+
f0()
61+
f1()
62+
f2()
63+
f3()
64+
}

0 commit comments

Comments
 (0)