上卷 程序设计
中卷 标准库
- 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 接口
接口类型分 iface 和 eface 两种。区别在于 iface 用 itab 实现存储方法指针的虚表(vtable),而 eface 仅存储类型信息,对应空接口( interface{}
)。
// runtime2.go type iface struct { tab *itab data unsafe.Pointer } type itab struct { inter *interfacetype _type *_type hash uint32 // copy of _type.hash. Used for type switches. _ [4]byte fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter. }
type eface struct { _type *_type data unsafe.Pointer }
用一简单示例探索接口创建和调用。
为简化汇编代码,使用默认优化模式。同时以编译指示阻止内联,阻止插入栈扩张检查代码。
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 段。
$ go build -gcflags "-S" "".makeface STEXT nosplit size=44 args=0x0 locals=0x10 funcid=0x0 align=0x0 TEXT "".makeface(SB), NOSPLIT|ABIInternal, $16-0 // 汇编代码。 SUBQ $16, SP MOVQ BP, 8(SP) LEAQ 8(SP), BP MOVL $256, AX // 0x100 CALL runtime.convT64(SB) // .data MOVQ AX, BX LEAQ go.itab."".data,"".Xer(SB), AX // return iface{ itab: AX, data: BX } MOVQ 8(SP), BP ADDQ $16, SP RET // 机器码。 0x0000 48 83 ec 10 48 89 6c 24 08 48 8d 6c 24 08 b8 00 H...H.l$.H.l$... 0x0010 01 00 00 e8 00 00 00 00 48 89 c3 48 8d 05 00 00 ........H..H.... 0x0020 00 00 48 8b 6c 24 08 48 83 c4 10 c3 ..H.l$.H.... rel 2+0 t=23 type."".data+0 // 按偏移量填充。 rel 20+4 t=7 runtime.convT64+0 // 偏移+长度 类型 目标符号 rel 30+4 t=14 go.itab."".data,"".Xer+0 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 ...M............ 0x0020 00 00 00 00 00 00 00 00 ........ rel 0+8 t=1 type."".Xer+0 // 接口类型。 rel 8+8 t=1 type."".data+0 // 目标类型。 rel 24+8 t=-32767 "".(*data).A+0 // 方法表。 rel 32+8 t=-32767 "".(*data).B+0
"".main STEXT size=54 args=0x0 locals=0x10 funcid=0x0 align=0x0 TEXT "".main(SB), ABIInternal, $16-0 SUBQ $16, SP MOVQ BP, 8(SP) LEAQ 8(SP), BP CALL "".makeface(SB) MOVQ 32(AX), AX // itab+32 -> *data.B MOVQ AX, CX // CX -> *data.B MOVQ BX, AX // AX -> *data -> receiver CALL CX MOVQ 8(SP), BP ADDQ $16, SP RET
从完整名称 go.itab."".data,"".Xer(SB)
,以及类型信息的填充数据可以看出。编译器为每个实现接口的类型专门生成一个 itab,因为其中同时包含了接口和目标类型信息。
另外,用专门函数复制目标对象,返回其指针存入 data 字段。
- convT16 , convT32 , convT64 : small types
- convTstring : string
- convTslice : slice
- convT , convTnoptr : general case
- convI2I
// 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, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, ... 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff, } func convT64(val uint64) (x unsafe.Pointer) { // 0 ~ 255 的小数字被缓存共享,无需额外分配内存。 if val < uint64(len(staticuint64s)) { x = unsafe.Pointer(&staticuint64s[val]) } else { // 堆上分配。这也是接口会导致内存逃逸的原因。 x = mallocgc(8, uint64Type, false) *(*uint64)(x) = val } return }
除小数字外,被缓存的还有空字符串("")和 nil。它们指向 map.go zeroVal 全局变量。
// iface.go func convT(t *_type, v unsafe.Pointer) unsafe.Pointer { // 堆内存分配,复制目标对象。 x := mallocgc(t.size, t, true) typedmemmove(t, x, v) return x }
如果 makeface 以指针实现接口呢?
//go:noinline //go:nosplit func makeface() Xer { var d data = 0x100 return &d }
"".makeface STEXT nosplit size=53 args=0x0 locals=0x18 funcid=0x0 align=0x0 TEXT "".makeface(SB), NOSPLIT|ABIInternal, $24-0 SUBQ $24, SP MOVQ BP, 16(SP) LEAQ 16(SP), BP // 局部变量 d 逃逸到堆。 LEAQ type."".data(SB), AX CALL runtime.newobject(SB) MOVQ $256, (AX) // 指针没必要转换,直接复制指针本身即可。 // return iface{ itab: AX, data: BX} MOVQ AX, BX LEAQ go.itab.*"".data,"".Xer(SB), AX MOVQ 16(SP), BP ADDQ $24, SP RET
测试
直接 itab + offset 定位方法表,从中拿到目标方法地址。与普通调用一样,并没太多开销。
问题在于:
- 静态调用,CPU 可预缓存指令分支,并发执行。而接口动态分发在运行期,吃亏。
- 接口实现方式决定了,无法使用函数内联。
- 接口可能导致堆上内存分配,此开销最大。
内联测试
package main type Xer interface { A() B() } type data int func (d data) A() { println("a") } func (d data) B() { println("b") } func main() { var d data = 100 d.A() var x Xer = d x.B() }
"".main STEXT size=75 args=0x0 locals=0x18 funcid=0x0 align=0x0 TEXT "".main(SB), ABIInternal, $24-0 SUBQ $24, SP MOVQ BP, 16(SP) LEAQ 16(SP), BP // 优化掉局部变量,内联 d.A()。 CALL runtime.printlock(SB) LEAQ go.string."a\n"(SB), AX MOVL $2, BX CALL runtime.printstring(SB) CALL runtime.printunlock(SB) // 优化掉接口创建,可惜没有内联 d.B()。 MOVL $100, AX CALL "".data.B(SB) MOVQ 16(SP), BP ADDQ $24, SP RET
逃逸测试
package main import ( "fmt" ) func main() { x := 100 fmt.Println(x) }
$ go build -gcflags "-m" ./main.go:9:13: inlining call to fmt.Println ./main.go:9:13: ... argument does not escape ./main.go:9:13: x escapes to heap
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论