资讯详情

GoLang之图解装箱

文章目录

  • GoLang之图解装箱
    • 1.问题引入
    • 2.问题探索
    • 3.探索结论
    • 4.值型装箱是否会堆放分配?

GoLang之图解装箱

1.问题引入

在这里插入图片描述

无论是空接口,我们都介绍了接口的数据结构interface{}或非空接口,本质上是两个指针:一个与类型元数据有关,另一个与接口装载数据有关。 有一个问题需要仔细探索:data字段是指针,那么它是如何接受一种值类型的赋值的呢?就像以下两个代码:

(1)给空接口类型给空接口类型变量e1:

n := 10 var e1 interface{ 
        } = n 

(2)将整形变量地址赋值空接口类型变量e2

n := 10 var e2 interface{ 
        } = &n 

现在,请考虑:e1和e分别长什么样? e2比较简单,_type指向int*类型元数据,data存储变量n的地址。 e1不一样,_type指向int类型元数据没有问题,但是data什么是指针存储?不能存储。n,因为是整形,而且data是个指针。 既然猜测不好,我们来探索一下反编译,亲眼看看。data存储什么?

2.问题探索

func v2e(n int) (e interface{ 
        }) { 
           e = n   return } 

反编译上述代码后,得到以下汇编代码:

 $ go tool objdump -S -s '^gom.v2e$' eface.o TEXT gom.v2e(SB) gofile..eface.go func v2e(n int) (e interface{ 
        }) { 
           0xb0a       65488b0c2528000000      MOVQ GS:0x28, CX   0xb13       488b8900000000          MOVQ 0(CX), CX          [3:7]R_TLS_LE   0xb1a       483b6110                CMPQ 0x10(CX), SP   0xb1e       763c JBE 0xb5c 0xb20 4883ec18 SUBQ $0x18, SP 0xb24 48896c2410 MOVQ BP, 0x10(SP) 0xb29 488d6c2410 LEAQ 0x10(SP), BP e = n 0xb2e 488b442420 MOVQ 0x20(SP), AX 0xb33 48890424 MOVQ AX, 0(SP) 0xb37 e800000000 CALL 0xb3c [1:5]R_CALL:runtime.convT64 0xb3c 488b442408 MOVQ 0x8(SP), AX return 0xb41 488d0d00000000 LEAQ 0(IP), CX [3:7]R_PCREL:type.int 0xb48 48894c2428 MOVQ CX, 0x28(SP) 0xb4d 4889442430 MOVQ AX, 0x30(SP) 0xb52 488b6c2410 MOVQ 0x10(SP), BP 0xb57 4883c418 ADDQ $0x18, SP 0xb5b c3 RET func v2e(n int) (e interface{ 
         }) { 
          0xb5c e800000000 CALL 0xb61 [1:5]R_CALL:runtime.morestack_noctxt 0xb61 eba7 JMP gom.v2e(SB) 

代码篇幅不太长,还是转换成等价的伪代码比较容易理解:

func v2e(n int) (e eface) { 
        
entry:
  gp := getg()
  if SP <= gp.stackguard0 { 
        
    goto morestack
  }
  e.data = runtime.convT64(n)//关键1
  e._type = &type.int //关键2
  return
morestack:
  runtime.morestack_noctxt()
  goto entry
}

忽略掉栈增长相关代码,我们真正感兴趣的就是为e的两个成员赋值的这两行代码: 1、先把变量n的值作为参数调用了runtime.convT64,并把返回值赋给了e.data,所以data存储的是runtime.convT64的返回值。 2、把type.int的地址赋给了e._type。这倒是比较容易理解,接下来就要看runtime.convT64的逻辑了:

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
}

主要逻辑:当val的值小于staticuint64s的长度时,直接返回staticuint64s中第val项的地址。否则就通过mallocgc分配一个uint64,把val的值赋给它并返回它的地址。这个staticuint64s是个长度为256的uint64数组,每个元素的值都跟下标一致,存储了0~255这256个值,主要是用来避免常用数字频繁堆分配。 整体来看convT64的功能,实际上就是堆分配一个uint64,并且将val参数作为初始值赋给它,然后返回它的地址。

3.探索结论

1、interface{}被设计成一个容器,但它本质上是指针,可以直接装载地址,用来实现装载值的话,实际的内存要分配在别的地方,并把内存地址存储在这里。(convT64的作用就是分配这个存储值的内存空间,实际上runtime中有一系列的这类函数,如convT32、convTstring和convTslice等。) 2、通过staticuint64s这种优化方式,能够反向推断出:被convT64分配的这个uint64,它的值在语义层面是不可修改的,是个类似const的常量,这样设计主要是为了跟interface{}配合来模拟“装载值”。 3、至于为什么这个值不可修改,因为interface{}只是一个容器,它支持把数据装入和取出,但是不支持直接在容器里修改。这有些类似于Java和C#里的自动装箱,只不过interface{}是个万能包装类。

4.值类型装箱就一定会堆分配吗?

这个问题也需要验证。既然已经知道逃逸会造成堆分配,那就构造一个值类型装箱但不逃逸的场景,就是如下代码中的fn函数:

func fn(n int) bool { 
        
  return notNil(n)
}

func notNil(a interface{ 
        }) bool { 
        
  return a != nil
}

编译时需要禁止内联优化,编译器还是能够通过notNil函数的代码实现判定没有发生逃逸,反编译fn得到如下汇编代码:


$ go tool objdump -S -s '^gom.fn$' eface.o
TEXT gom.fn(SB) gofile..eface.go
func fn(n int) bool { 
        
  0xfd6         65488b0c2528000000      MOVQ GS:0x28, CX
  0xfdf         488b8900000000          MOVQ 0(CX), CX          [3:7]R_TLS_LE
  0xfe6         483b6110                CMPQ 0x10(CX), SP
  0xfea         764a                    JBE 0x1036
  0xfec         4883ec28                SUBQ $0x28, SP
  0xff0         48896c2420              MOVQ BP, 0x20(SP)
  0xff5         488d6c2420              LEAQ 0x20(SP), BP
        return notNil(n)
  0xffa         488b442430              MOVQ 0x30(SP), AX
  0xfff         4889442418              MOVQ AX, 0x18(SP)
  0x1004        488d0500000000          LEAQ 0(IP), AX          [3:7]R_PCREL:type.int
  0x100b        48890424                MOVQ AX, 0(SP)
  0x100f        488d442418              LEAQ 0x18(SP), AX
  0x1014        4889442408              MOVQ AX, 0x8(SP)
  0x1019        e800000000              CALL 0x101e             [1:5]R_CALL:gom.notNil
  0x101e        0fb6442410              MOVZX 0x10(SP), AX
  0x1023        88442438                MOVB AL, 0x38(SP)
  0x1027        488b6c2420              MOVQ 0x20(SP), BP
  0x102c        4883c428                ADDQ $0x28, SP
  0x1030        c3                      RET
func fn(n int) bool { 
        
  0x1031        0f1f440000              NOPL 0(AX)(AX*1)
  0x1036        e800000000              CALL 0x103b             [1:5]R_CALL:runtime.morestack_noctxt
  0x103b        eb99                    JMP gom.fn(SB)

把上面的代码转换为等价的伪代码: 注意伪代码中的局部变量v,它实际上是被编译器隐式分配的(可没在堆上哦~),被用作n的值拷贝。

func fn(n int) bool { 
        
entry:
  gp := getg()
  if SP <= gp.stackguard0 { 
        
    goto morestack
  }
  v := n
  return notNil(eface{ 
        _type: &type.int, data: &v})
morestack:
  runtime.morestack_noctxt()
  goto entry
}

所以,interface{}装载值的时候,必须单独拷贝一份,而不能直接让data存储原始变量的地址,但是否堆分配还是要经过逃逸分析:值类型装箱后又涉及到逃逸的情况时,才会用到runtime中的一系列convT函数。

标签: 101e电位器trimmer

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台