返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

7.1 实现

发布于 2024-10-12 19:15:48 字数 3636 浏览 0 评论 0 收藏 0

接口本身一样是静态类型,内部使用 itab 结构存储运行期所需的相关类型信息。

更多细节,可参考《源码剖析,其他》。

// runtime/runtime2.go

type iface struct {
	tab  *itab            // 类型和方法表。
	data unsafe.Pointer   // 目标对象指针。
}

type itab struct {
	inter *interfacetype  // 接口类型。  
	_type *_type          // 目标类型。
	hash  uint32
	_     [4]byte
    fun   [1]uintptr      // 方法表。 offset: 24, 0x18
}

内部实现

输出编译结果,查看接口结构存储的具体内容。

type Mer interface {
	A()	
}

// ---------------------

type Ner interface {
	Mer

	B(int)
	C(string) string
}

// ---------------------

type N int

func  (N) A() {}
func (*N) B(int) {}
func (*N) C(string) string { return "" }
func (*N) D() {}

// ---------------------

func main() {
	var n N
	var t Ner = &n
	t.A()
}
$ go build -gcflags "-N -l -S"

"".main STEXT
  LEAQ	go.itab.*"".N,"".Ner(SB), SI

go.itab.*"".N,"".Ner SRODATA
	0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
	0x0010 57 9a 14 c4 00 00 00 00 00 00 00 00 00 00 00 00
	0x0020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
	rel  0+8 t=1       type."".Ner+0
	rel  8+8 t=1       type.*"".N+0
	rel 24+8 t=-32767  "".(*N).A+0
	rel 32+8 t=-32767  "".(*N).B+0
	rel 40+8 t=-32767  "".(*N).C+0   ; 不含接口未声明的 *N.D 

编译器将接口和目标类型组合,生成实例( go.itab.*"".N,"".Ner )。

其中有接口和目标类型引用,并在方法表(24, 0x18)静态填入具体方法地址。

前文提及,接口有个重要特征:赋值给接口时,会复制目标对象。

func main() {
	var n N = 100
	
	var t1 Mer = n   // copy n
	t1.A()

	var t2 Ner = &n  // copy ptr
	t2.B(1)
}
(gdb) info locals

t1 = {
  tab = 0x477d38 <N,main.Mer>,
  data = 0xc000032730            // data = &n_copy
}

t2 = {
  tab = 0x477d78 <N,main.Ner>,
  data = 0xc000032728            // data = &n
}

n = 100

(gdb) p/x &n
$1 = 0xc000032728

(gdb) x/1xg 0xc000032730        ; t1.data。
0xc000032730:   0x0000000000000064

因不可寻址(unaddressable),故无法修改接口内部存储的复制品。

func main() {
	var n N = 100

	// var t1 Mer = n
	// p := &(t1.(N))
	//      ~~~~~~~~ cannot take address

	var t2 Ner = &n    // 接口存储的是指针。
    *(t2.(*N)) = 200   // 透过指针修改目标对象。

	println(n) // 200
}

动态调用

以接口调用方法,需通过方法表动态完成。

//go:noinline
func test(n Ner) {
	n.B(9)
}

func main() {
	var n N = 100
	var i Ner = &n
	test(i)
}
# 相比动态调用,内存逃逸才是接口导致的最大性能问题。


$ go build -gcflags "-m"

    moved to heap: n


$ go tool objdump -S -s "main\.main" ./test

func main() {

        var n N = 100
        
  0x455294    LEAQ 0x5d45(IP), AX
  0x45529b    NOPL 0(AX)(AX*1)
  0x4552a0    CALL runtime.newobject(SB)  ; heap alloc
  0x4552a5    MOVQ $0x64, 0(AX)
  
        test(i)
        
  0x4552ac    MOVQ AX, BX                 ; .data
  0x4552af    LEAQ go.itab.*main.N,main.Ner(SB), AX
  0x4552b6    CALL main.test(SB)
}


$ go tool objdump -S -s "main\.test" ./test

func test(n Ner) {

        n.B(9)
        
  0x45523e    MOVQ 0x20(AX), CX  ; .itab + 32 -> B()
  0x455242    MOVQ BX, AX        ; .data
  0x455245    MOVL $0x9, BX      ; argument
  0x45524a    CALL CX            ; B(.data, 0x9)
}

编译器尝试以内联等方式优化,消除(或减少)因动态调用和内存逃逸导致的性能问题。

func test(n Ner) {
    n.B(9)
}

func main() {
    var n N = 100
    var i Ner = &n
    test(i)
}

优化后的代码,完全抹掉了接口的痕迹(内存逃逸和动态调用)。

$ go build -gcflags "-m"

    can inline test
    can inline main

    inlining call to test


$ go tool objdump -S -s "main\.main" ./test

func main() {

        var n N = 100
        
  0x455234    MOVQ $0x64, 0x10(SP)
  
        n.B(9)
        
  0x45523e    LEAQ 0x10(SP), AX
  0x455243    MOVL $0x9, BX
  0x455248    CALL main.(*N).B(SB)
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

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

发布评论

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