上卷 程序设计
中卷 标准库
- bufio 1.18
- bytes 1.18
- io 1.18
- container 1.18
- encoding 1.18
- crypto 1.18
- hash 1.18
- index 1.18
- sort 1.18
- context 1.18
- database 1.18
- connection
- query
- queryrow
- exec
- prepare
- transaction
- scan & null
- context
- tcp
- udp
- http
- server
- handler
- client
- h2、tls
- url
- rpc
- exec
- signal
- embed 1.18
- plugin 1.18
- reflect 1.18
- runtime 1.18
- KeepAlived
- ReadMemStats
- SetFinalizer
- Stack
- sync 1.18
- atomic
- mutex
- rwmutex
- waitgroup
- cond
- once
- map
- pool
- copycheck
- nocopy
- unsafe 1.18
- fmt 1.18
- log 1.18
- math 1.18
- time 1.18
- timer
下卷 运行时
源码剖析
附录
8.2 interface
接口类型分 iface 和 eface 两种。
// runtime2.go type iface struct { tab *itab data unsafe.Pointer } type eface struct { _type *_type data unsafe.Pointer }
区别在于 iface 用 itab 实现存储方法指针的虚表(vtable)。
而 eface 仅存储类型信息,对应空接口( interface{}
)。
编译器(链接)将类型信息保存到 .rodata 里。
type itab struct { // offset inter *interfacetype // 00: 接口类型信息。 _type *_type // 08: 实现接口的类型信息。 hash uint32 // 16: copy of _type.hash. Used for type switches. _ [4]byte // 20: fun [1]uintptr // 24: variable sized. fun[0]==0 means _type does not implement inter. }
用一个简单示例探索接口创建和调用。
为了简化汇编代码,使用默认优化模式。
但使用编译指示阻止内联和插入栈扩张代码。
package main type Xer interface { A() B() } type data int //go:noinline func (d data) A() {} //go:noinline func (d data) B() {} //go:noinline //go:nosplit func makeface() Xer { var d data = 0x100 return d } func main() { x := makeface() x.B() }
创建
编译时,直接将接口类型(itab)数据存放于 .rodata 段。
使用 -S 参数,查看输出结果。
从符号名(go.itab.main.data,main.Xer)可以看出,编译器将目标类型和接口类型整合成一个 itab。
随后将 itab 指针和 data 填充到 iface 变量。
$ go build -gcflags -S "".makeface STEXT nosplit size=59 args=0x10 locals=0x18 0x0000 SUBQ $24, SP SP 0 +-------------+----- 0x0004 MOVQ BP, 16(SP) | 256 | 0x0009 LEAQ 16(SP), BP 8 +-------------+ | convT64 ret | makeface 0x000e MOVQ $256, (SP) BP 16 +-------------+ 0x0016 CALL runtime.convT64(SB) | main.BP | 0x001b MOVQ 8(SP), AX 24 +-------------+----- | main.IP | 0x0020 LEAQ go.itab."".data,"".Xer(SB), CX 32 +-------------+----- 0x0027 MOVQ CX, "".~r0+32(SP) // .tab | .tab | 0x002c MOVQ AX, "".~r0+40(SP) // .data 40 +-------------+ | .data | main 0x0031 MOVQ 16(SP), BP +-------------+ 0x0036 ADDQ $24, SP | ... | 0x003a RET +-------------+----- "".main STEXT size=70 args=0x0 locals=0x18 0x000f SUBQ $24, SP SP 0 +--------------+----- 0x0013 MOVQ BP, 16(SP) | .tab | 0x0018 LEAQ 16(SP), BP 8 +--------------+ | .data | main 0x001d CALL "".makeface(SB) 16 +--------------+ 0x0022 MOVQ (SP), AX | ... | 0x0026 MOVQ 8(SP), CX +--------------+---- 0x002b MOVQ 32(AX), AX 0x002f MOVQ CX, (SP) => .tab+32 --> (*data).B --> AX 0x0033 CALL AX => .data --> CX --> (SP) => B(.data) 0x0035 MOVQ 16(SP), BP 0x003a ADDQ $24, SP 0x003e RET go.itab."".data,"".Xer SRODATA dupok size=40 0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0010 ef 0c d1 4d 00 00 00 00 00 00 00 00 00 00 00 00 0x0020 00 00 00 00 00 00 00 00 rel 0+8 t=1 type."".Xer+0 // 按偏移量填充 itab 字段。 rel 8+8 t=1 type."".data+0 // rel 偏移+长度 类型 目标符号(t=1 表示地址类型)。 rel 24+8 t=1 "".(*data).A+0 // 填充方法指针。 rel 32+8 t=1 "".(*data).B+0
相关函数
复制对象,返回指针。
- convT2E : interface{}
- convT16 , convT32 , convT64 : small types
- convTstring : string
- convTslice : slice
- convT2I : general case
- convT2Inoptr : structs that do not contain pointers
函数实现很简单。在堆上分配内存,复制为 data 对象。
如果是 struct 等复合结构,会使用 memmove 复制。
// iface.go func convT64(val uint64) (x unsafe.Pointer) { if val < uint64(len(staticuint64s)) { x = unsafe.Pointer(&staticuint64s[val]) } else { x = mallocgc(8, uint64Type, false) *(*uint64)(x) = val } return }
func convT32(val uint32) (x unsafe.Pointer) { if val < uint32(len(staticuint64s)) { x = unsafe.Pointer(&staticuint64s[val]) } else { x = mallocgc(4, uint32Type, false) *(*uint32)(x) = val } return }
1.15 缓存了小整数 0 ~ 255,避免在堆上重复分配内存。
类似的还有 nil 和 "",也使用预置的 zeroVal。
// iface.go // staticuint64s is used to avoid allocating in convTx for small integer values. var staticuint64s = [...]uint64{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, ... 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff, }
如果 makeface 以指针实现接口呢?
// test.go //go:noinline //go:nosplit func makeface() Xer { var d data = 0x100 return &d }
因为复制的是指针,所以没必要调用 conv 函数。
$ go tool objdump -s "main\.makeface" test TEXT main.makeface(SB) test.go:18 SUBQ $0x18, SP test.go:18 MOVQ BP, 0x10(SP) test.go:18 LEAQ 0x10(SP), BP test.go:19 LEAQ type.*+39232(SB), AX // 堆上分配 d 内存。 test.go:19 MOVQ AX, 0(SP) test.go:19 CALL runtime.newobject(SB) test.go:19 MOVQ 0x8(SP), AX test.go:19 MOVQ $0x100, 0(AX) test.go:20 LEAQ go.itab.*main.data,main.Xer(SB), CX // 填充 iface 字段。 test.go:20 MOVQ CX, 0x20(SP) // .tab test.go:20 MOVQ AX, 0x28(SP) // .data test.go:20 MOVQ 0x10(SP), BP test.go:20 ADDQ $0x18, SP test.go:20 RET
调试
利用调试器,查看 itab 信息。
(gdb) l main.main 18 func makeface() Xer { 19 var d data = 0x100 20 return d 21 } 22 23 func main() { 24 x := makeface() 25 x.B() 26 } (gdb) b 25 Breakpoint 1 at 0x44fa65: file test.go, line 25. (gdb) r Starting program: test Thread 1 "test" hit Breakpoint 1, main.main () at test.go:25 25 x.B() (gdb) p/x x $1 = { tab = 0x47db80, data = 0xc000062000 } (gdb) p/x *x.tab $2 = { inter = 0x460080, _type = 0x45ef80, hash = 0x4dd10cef, _ = {0x0, 0x0, 0x0, 0x0}, fun = {0x44faf0} } (gdb) p/x *x.tab.inter $3 = { typ = { size = 0x10, ptrdata = 0x10, hash = 0x6aeece04, tflag = 0x7, align = 0x8, fieldalign = 0x8, kind = 0x14, alg = 0x4bff20, gcdata = 0x47bbe1, str = 0x1c0c, ptrToThis = 0x7340 }, pkgpath = { bytes = 0x4502d0 }, mhdr = []runtime.imethod = {{ name = 0x3, ityp = 0xa460 }, { name = 0x7, ityp = 0xa460 }} } (gdb) x/xg x.tab.fun[0] 0x44faf0 <main.(*data).A>: 0xfffff8250c8b4864 (gdb) x/xg x.tab.fun[1] 0x44fb60 <main.(*data).B>: 0xfffff8250c8b4864
自动生成的包装方法。
1.14: All Go symbols in macOS binaries now begin with an underscore, following platform conventions.
$ go tool objdump test | grep "^TEXT main\." TEXT main.data.A(SB) TEXT main.data.B(SB) TEXT main.makeface(SB) TEXT main.main(SB) TEXT main.(*data).A(SB) <autogenerated> TEXT main.(*data).B(SB) <autogenerated>
调用
编译器直接通过对 itab 偏移定位,从方法表中拿到目标方法地址。
然后和普通调用一样准备所需参数即可。
接口调用成本开销不算大。
但问题在于:
1. 静态调用,CPU 可通过地址预缓存指令分支,甚至并发执行。接口(参数)动态分发在运行期才知道,比较吃亏。
2. 接口实现方式决定了,无法使用函数内联。这应该是性能最大差别所在。
3. 接口需要在堆上分配内存,这块开销可能是最大的。
$ go tool objdump -s "main\.main" test TEXT main.main(SB) main.go:23 SUBQ $0x18, SP main.go:23 MOVQ BP, 0x10(SP) main.go:23 LEAQ 0x10(SP), BP main.go:24 CALL main.makeface(SB) // 返回 iface main.go:24 MOVQ 0(SP), AX // 获取 iface.tab main.go:24 MOVQ 0x8(SP), CX // 获取 iface.data main.go:25 MOVQ 0x20(AX), AX // 获取 iface.fun[1],main.data.B main.go:25 MOVQ CX, 0(SP) // 0x20 = $32 main.go:25 CALL AX main.go:26 MOVQ 0x10(SP), BP main.go:26 ADDQ $0x18, SP main.go:26 RET
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论