返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

8.2 interface

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

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

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

发布评论

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