返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

8.2 接口

发布于 2024-10-12 19:16:00 字数 5920 浏览 0 评论 0 收藏 0

接口类型分 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文