github.com/code-reading/golang@v0.0.0-20220303082512-ba5bc0e589a3/docs/006-unsafe包源码分析.md (about)

     1  unsafe 包绕过了 Go 的类型系统,达到直接操作内存的目的,使用它有一定的风险性。但是在某些场景下,使用 unsafe 包提供的函数会提升代码的效率,Go 源码中也是大量使用 unsafe 包。
     2  
     3  通过 unsafe 相关函数,可以获取结构体私有成员的地址,进而对其做进一步的读写操作,突破 Go 的类型安全限制。
     4  
     5  unsafe 包定义了 Pointer 和三个函数:
     6  
     7  ```go
     8  type ArbitraryType int	
     9  type Pointer *ArbitraryType	
    10  func Sizeof(x ArbitraryType) uintptr	
    11  func Offsetof(x ArbitraryType) uintptr	
    12  func Alignof(x ArbitraryType) uintptr
    13  ```
    14  - Arbitrary 是任意的意思,也就是说 Pointer 可以指向任意类型,实际上它类似于 C 语言里的 void*。
    15  
    16  - Sizeof 返回类型 x 所占据的字节数,但不包含 x 所指向的内容的大小。例如,对于一个指针,函数返回的大小为 8 字节(64位机上),一个 slice 的大小则为 slice header 的大小。
    17  
    18  - Offsetof 返回结构体成员在内存中的位置离结构体起始处的字节数,所传参数必须是结构体的成员。
    19  
    20  - Alignof Alignof返回的对齐数是结构体中最大元素所占的内存数,不超过8。
    21  
    22  通过三个函数可以获取变量的大小、偏移、对齐等信息。
    23  
    24  注意到以上三个函数返回的结果都是 uintptr 类型,这和 unsafe.Pointer 可以相互转换。三个函数都是在编译期间执行,它们的结果可以直接赋给 const型变量。另外,因为三个函数执行的结果和操作系统、编译器相关,所以是不可移植的。
    25  
    26  综上所述,unsafe 包提供了 2 点重要的能力:
    27  
    28  > 任何类型的指针和 unsafe.Pointer 可以相互转换。
    29  
    30  > uintptr 类型和 unsafe.Pointer 可以相互转换。
    31  
    32  uintptr 可以和 unsafe.Pointer 进行相互转换,uintptr 可以进行数学运算。这样,通过 uintptr 和 unsafe.Pointer 的结合就解决了 Go 指针不能进行数学运算的限制。
    33  
    34  uintptr 并没有指针的语义,意思就是 uintptr 所指向的对象会被 gc 无情地回收。而 unsafe.Pointer 有指针语义,可以保护它所指向的对象在“有用”的时候不会被垃圾回收。
    35  
    36  pointer 不能直接进行数学运算,但可以把它转换成 uintptr,对 uintptr 类型进行数学运算,再转换成 pointer 类型。
    37  
    38  uintptr 是一个地址数值,它不是指针,与地址上的对象没有引用关系,垃圾回收器不会因为有一个uintptr类型的值指向某对象而不回收该对象。
    39  
    40  unsafe.Pointer是一个指针,类似于C的void *,它与地址上的对象存在引用关系,垃圾回收器会因为有一个unsafe.Pointer类型的值指向某对象而不回收该对象。
    41  
    42  任何指针都可以转为unsafe.Pointer
    43  
    44  unsafe.Pointer可以转为任何指针
    45  
    46  uintptr可以转换为unsafe.Pointer
    47  
    48  unsafe.Pointer可以转换为uintptr
    49  
    50  指针不能直接转换为uintptr
    51  
    52  Offsetof 获取成员偏移量
    53  
    54  对于一个结构体,通过 offset 函数可以获取结构体成员的偏移量,进而获取成员的地址,读写该地址的内存,就可以达到改变成员值的目的。
    55  
    56  这里有一个内存分配相关的事实:结构体会被分配一块连续的内存,结构体的地址也代表了第一个成员的地址。
    57  
    58  ```go
    59  package main	
    60  import (	
    61      "fmt"	
    62      "unsafe"	
    63  )	
    64  type Programmer struct {	
    65      name string	
    66      language string	
    67  }	
    68  func main() {	
    69      p := Programmer{"stefno", "go"}	
    70      fmt.Println(p)	
    71      name := (*string)(unsafe.Pointer(&p))	
    72      *name = "qcrao"	
    73      lang := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Offsetof(p.language)))	
    74      *lang = "Golang"	
    75      fmt.Println(p)	
    76  }
    77  ```
    78  output 
    79  
    80  ```shell 
    81  {stefno go}	
    82  {qcrao Golang}
    83  ```
    84  
    85  name 是结构体的第一个成员,因此可以直接将 &p 解析成 *string。这一点,在前面获取 map 的 count 成员时,用的是同样的原理。
    86  
    87  对于结构体的私有成员,现在有办法可以通过 unsafe.Pointer 改变它的值了。
    88  
    89  私有成员变量
    90  
    91  ```go
    92  type Programmer struct {	
    93      name string	
    94      age int	
    95      language string	
    96  }
    97  
    98  func main() {	
    99      p := Programmer{"stefno", 18, "go"}	
   100      fmt.Println(p)	
   101      lang := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Sizeof(int(0)) + unsafe.Sizeof(string(""))))	
   102      *lang = "Golang"	
   103      fmt.Println(p)	
   104  }
   105  
   106  ``` 
   107  
   108  output 
   109  
   110  ```shell 
   111  {stefno 18 go}	
   112  {stefno 18 Golang}
   113  ```
   114  
   115  string 和 slice 的相互转换
   116  
   117  这是一个非常精典的例子。实现字符串和 bytes 切片之间的转换,要求是 zero-copy。想一下,一般的做法,都需要遍历字符串或 bytes 切片,再挨个赋值。
   118  
   119  完成这个任务,我们需要了解 slice 和 string 的底层数据结构
   120  
   121  ```go 
   122  type StringHeader struct {	
   123      Data uintptr	
   124      Len  int	
   125  }	
   126  type SliceHeader struct {	
   127      Data uintptr	
   128      Len  int	
   129      Cap  int	
   130  }
   131  ```
   132  
   133  上面是反射包下的结构体,路径:src/reflect/value.go。只需要共享底层 []byte 数组就可以实现 zero-copy
   134  
   135  ```go
   136  func string2bytes(s string) []byte {	
   137      stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))	
   138      bh := reflect.SliceHeader{	
   139          Data: stringHeader.Data,	
   140          Len:  stringHeader.Len,	
   141          Cap:  stringHeader.Len,	
   142      }	
   143      return *(*[]byte)(unsafe.Pointer(&bh))	
   144  }	
   145  func bytes2string(b []byte) string{	
   146      sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))	
   147      sh := reflect.StringHeader{	
   148          Data: sliceHeader.Data,	
   149          Len:  sliceHeader.Len,	
   150      }	
   151      return *(*string)(unsafe.Pointer(&sh))	
   152  }
   153  ```
   154  
   155  通过构造 slice header 和 string header,来完成 string 和 byte slice 之间的转换
   156  
   157  
   158  需要注意的是 栈增长时栈可能会发生移动, 如下 
   159  
   160  ```go
   161  var obj int
   162  fmt.Println(uintptr(unsafe.Pointer(&obj)))
   163  bigFunc() // bigFunc()增大了栈
   164  fmt.Println(uintptr(unsafe.Pointer(&obj)))
   165  ```
   166  
   167  完全有可能打印出两个不同的地址 
   168  
   169  建议多用atomic 包 
   170  
   171  另外一个示例 
   172  
   173  ```go
   174  package main
   175  
   176  type Person struct {
   177  	Name string
   178  	age  int
   179  }
   180  
   181  func main() {
   182  	p := &Person{
   183  		Name: "张三",
   184  	}
   185  
   186  	fmt.Println(p)
   187  	// *Person是不能直接转换为*string的,所以这里先将*Person转为unsafe.Pointer,再将unsafe.Pointer转为*string
   188  	pName := (*string)(unsafe.Pointer(p)) 
   189  	*pName = "李四"
   190  
   191  	// 正常手段是不能操作Person.age的这里先通过uintptr(unsafe.Pointer(pName))得到Person.Name的地址
   192  	// 通过unsafe.Sizeof(p.Name)得到Person.Name占用的字节数
   193  	// Person.Name的地址 + Person.Name占用的字节数就得到了Person.age的地址,然后将地址转为int指针。
   194  	pAge := (*int)(unsafe.Pointer((uintptr(unsafe.Pointer(pName)) + unsafe.Sizeof(p.Name))))
   195  	// 将p的age字段修改为12
   196  	*pAge = 12
   197  
   198  	fmt.Println(p)
   199  }
   200  ```
   201  
   202  为什么需要uintptr这个类型呢?
   203  
   204  理论上说指针不过是一个数值,即一个uint,但实际上在go中unsafe.Pointer是不能通过强制类型转换为一个uint的,只能将unsafe.Pointer强制类型转换为一个uintptr。
   205  
   206  ```go
   207  var v1 float64 = 1.1
   208  var v2 *float64 = &v1
   209  _ = int(v2) // 这里编译报错:cannot convert unsafe.Pointer(v2) (type unsafe.Pointer) to type uint
   210  ```
   211  
   212  但是可以将一个unsafe.Pointer强制类型转换为一个uintptr:
   213  
   214  ```go
   215  var v1 float64 = 1.1
   216  var v2 *float64 = &v1
   217  var v3 uintptr = uintptr(unsafe.Pointer(v2))
   218  v4 := uint(v3)
   219  fmt.Println(v3, v4) // v3和v4打印出来的值是相同的
   220  ```
   221  
   222  可以理解为uintptr是专门用来指针操作的uint。
   223  
   224  另外需要指出的是指针不能直接转为uintptr,即
   225  
   226  ```go
   227  var a float64
   228  uintptr(&a) 这里会报错,不允许将*float64转为uintptr
   229  ```
   230  
   231  unsafe.Sizeof 和 unsafe.Offsetof 的理解
   232  
   233  
   234  unsafe.Sizeof 返回这个字段占用的字节数 
   235  
   236  unsafe.Offsetof 返回这个字段 距离结构体其实地址的偏移位置 
   237  
   238  ```go
   239  package main
   240    
   241   import (
   242       "fmt"
   243       "unsafe"
   244       "log"
   245   )
   246    
   247   func main() {
   248    
   249       var x struct {
   250           a bool
   251           b int16
   252           c []int
   253       }
   254    
   255       /**
   256       unsafe.Offsetof 函数的参数必须是一个字段 x.f, 然后返回 f 字段相对于 x 起始地址的偏移量, 包括可能的空洞.
   257       */
   258    
   259       /**
   260       uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
   261       指针的运算
   262       */
   263       // 和 pb := &x.b 等价
   264    
   265       pb := (*int16)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
   266       *pb = 42
   267       fmt.Println(x.b) // "42"
   268        
   269       //m := x{}
   270       x.a = true
   271       x.b = 132
   272       x.c = []int{1,2,2,3,4}
   273       log.Println("Sizeof:")
   274       log.Println(unsafe.Sizeof(x.a))
   275       log.Println(unsafe.Sizeof(x.b))
   276       log.Println(unsafe.Sizeof(x.c))
   277       log.Println(unsafe.Sizeof(x))
   278       log.Println("Offsetof:")
   279       log.Println(unsafe.Offsetof(x.a))
   280       log.Println(unsafe.Offsetof(x.b))
   281       log.Println(unsafe.Offsetof(x.c))
   282       log.Println("ttt:")
   283       type SizeOfE struct {
   284         A byte  // 1
   285         C byte  // 1 //调换一下B C的顺序,择Sizeof 整个结构体的大小由24变为了16
   286         B int64 // 8
   287      }
   288      var tt SizeOfE
   289      log.Println(unsafe.Sizeof(SizeOfE{}))
   290      log.Println(unsafe.Sizeof(tt.A))
   291      log.Println(unsafe.Sizeof(tt.B))
   292      log.Println(unsafe.Sizeof(tt.C))
   293      log.Println(unsafe.Sizeof(tt))
   294      log.Println("AlignOf:")
   295      log.Println(unsafe.Alignof(tt.A))
   296      log.Println(unsafe.Alignof(tt.B))
   297      log.Println(unsafe.Alignof(tt.C))
   298      log.Println(unsafe.Alignof(tt))
   299   
   300  }
   301  ```
   302  
   303  如何得到一个对象所占内存大小?
   304  
   305  ```go
   306  fmt.Println(unsafe.Sizeof(int64(0))) // "8"
   307  
   308  type SizeOfA struct {
   309      A int
   310  }
   311  unsafe.Sizeof(SizeOfA{0}) // 8
   312  
   313  type SizeOfC struct {
   314      A byte  // 1字节
   315      C int32 // 4字节
   316  }
   317  unsafe.Sizeof(SizeOfC{0, 0})    // 8 返回占用的字节数
   318  unsafe.Alignof(SizeOfC{0, 0}) // 4  返回内存对齐之后,占用字节数最大的字段, 但是不超过8 
   319  结构体中A byte占1字节,C int32占4字节. SizeOfC占8字节
   320  ``` 
   321  内存对齐
   322  
   323  为何会有内存对齐?
   324  > 1.并不是所有硬件平台都能访问任意地址上的任意数据。
   325  
   326  > 2.性能原因 访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存只需访问一次。
   327  
   328  上面代码SizeOfC中元素一共5个字节,而实际结构体占8字节
   329  
   330  是因为这个结构体的对齐倍数Alignof(SizeOfC) = 4.也就是说,结构体占的实际大小必须是4的倍数,也就是8字节。
   331  
   332  ```go
   333  type SizeOfD struct {
   334      A byte
   335      B [5]int32
   336  }
   337  unsafe.Sizeof(SizeOfD{})   // 24
   338  unsafe.Alignof(SizeOfD{})  // 4
   339  ```
   340  
   341  > Alignof返回的对齐数是结构体中最大元素所占的内存数,不超过8,如果元素是数组那么取数组类型所占的内存值而不是整个数组的值
   342  
   343  ```go
   344  type SizeOfE struct {
   345      A byte  // 1
   346      B int64 // 8
   347      C byte  // 1
   348  }
   349  unsafe.Sizeof(SizeOfE{})    // 24
   350  unsafe.Alignof(SizeOfE{}) // 8
   351  ```
   352  
   353  > SizeOfE中,元素的大小分别为1,8,1,但是实际结构体占24字节,远超元素实际大小,因为内存对齐原因,最开始分配的8字节中包含了1字节的A,剩余的7字节不足以放下B,又为B分配了8字节,剩余的C独占再分配的8字节。
   354  
   355  ```go
   356  type SizeOfE struct {
   357      A byte  // 1
   358      C byte  // 1
   359      B int64 // 8    
   360  }
   361  unsafe.Sizeof(SizeOfE{})    // 16
   362  unsafe.Alignof(SizeOfE{}) // 8
   363  ```
   364  
   365  > 换一种写法,把A,C放到上面,B放到下面。这时SizeOfE占用的内存变为了16字节。因为首先分配的8字节足以放下A和C,省去了8字节的空间。
   366  上面一个结构体中元素的不同顺序足以导致内存分配的巨大差异。前一种写法产生了很多的内存空洞,导致结构体不够紧凑,造成内存浪费。
   367  
   368  这里建议在构建结构体时,按照字段大小的升序进行排序,会减少一点的内存空间。
   369  
   370  unsafe.Offsetof:返回结构体中元素所在内存的偏移量
   371  
   372  ```go
   373  type SizeOfF struct {
   374      A byte
   375      C int16
   376      B int64
   377      D int32
   378  }
   379  unsafe.Offsetof(SizeOfF{}.A) // 0
   380  unsafe.Offsetof(SizeOfF{}.C) // 2
   381  unsafe.Offsetof(SizeOfF{}.B) // 8
   382  unsafe.Offsetof(SizeOfF{}.D) // 16
   383  ```
   384  
   385  ![SizeOfF 内存布局图](../resources/images/sizeoff.png)
   386  SizeOfF 内存布局图, 蓝色区域是元素实际所占内存,灰色为内存空洞。
   387  
   388  下面总结一下go语言中各种类型所占内存大小(x64环境下):
   389  X64下1机器字节=8字节
   390  
   391  ![Golang内置类型占用内存大小](../resources/images/memlayout.png)
   392  Golang内置类型占用内存大小
   393  
   394  
   395  > 从例子中可以看出,结构体中元素不同顺序的排列会导致内存分配的极大差异,不好的顺序会产生许多的内存空洞,造成大量内存浪费。
   396  
   397  > 虽然这几个函数都在unsafe包中,但是他们并不是不安全的。在需要优化内存空间时这几个函数非常有用。
   398  
   399  
   400  反射包 
   401  
   402  ```go
   403  unsafe.Alignof(w)等价于reflect.TypeOf(w).Align
   404  unsafe.Alignof(w.i)等价于reflect.Typeof(w.i).FieldAlign()
   405  ```
   406  
   407  对齐Alignof
   408  
   409  要了解这个函数,你需要了解数据对齐。简单的说,它让数据结构在内存中以某种的布局存放,是该数据的读取性能能够更加的快速。
   410  
   411  CPU 读取内存是一块一块读取的,块的大小可以为 2、4、6、8、16 字节等大小。块大小我们称其为内存访问粒度。
   412  
   413  普通字段的对齐值
   414  
   415  ```go
   416  fmt.Printf("bool align: %d\n", unsafe.Alignof(bool(true)))
   417  fmt.Printf("int32 align: %d\n", unsafe.Alignof(int32(0)))
   418  fmt.Printf("int8 align: %d\n", unsafe.Alignof(int8(0)))
   419  fmt.Printf("int64 align: %d\n", unsafe.Alignof(int64(0)))
   420  fmt.Printf("byte align: %d\n", unsafe.Alignof(byte(0)))
   421  fmt.Printf("string align: %d\n", unsafe.Alignof("EDDYCJY"))
   422  fmt.Printf("map align: %d\n", unsafe.Alignof(map[string]string{}))
   423  ```
   424  
   425  output 
   426  ```go
   427  bool align: 1
   428  int32 align: 4
   429  int8 align: 1
   430  int64 align: 8
   431  byte align: 1
   432  string align: 8
   433  map align: 8
   434  ```
   435  在 Go 中可以调用 unsafe.Alignof 来返回相应类型的对齐系数。通过观察输出结果,可得知基本都是 2n,最大也不会超过 8。这是因为我们的64位编译器默认对齐系数是 8,因此最大值不会超过这个数。
   436  
   437  对齐规则
   438  
   439  - 结构体的成员变量,第一个成员变量的偏移量为 0。往后的每个成员变量的对齐值必须为编译器默认对齐长度(#pragma pack(n))或当前成员变量类型的长度(unsafe.Sizeof),取最小值作为当前类型的对齐值。其偏移量必须为对齐值的整数倍
   440  
   441  - 结构体本身,对齐值必须为编译器默认对齐长度或结构体的所有成员变量类型中的最大长度,取最大数的最小整数倍作为对齐值
   442  
   443  结合以上两点,可得知若编译器默认对齐长度超过结构体内成员变量的类型最大长度时,默认对齐长度是没有任何意义的
   444  
   445  结构体的对齐值
   446  下面来看一下结构体的对齐:
   447  ```go
   448  type part struct {
   449  	a bool  // 1
   450  	b int32 //4
   451  	c int8  // 1
   452  	d int64 // 8
   453  	e byte  // 1
   454  }
   455  
   456  func main() {
   457  	var p part
   458  	fmt.Println(unsafe.Sizeof(p)) // 32
   459  }
   460  ```
   461  
   462  按照普通字段(结构体内成员变量)的对齐方式,我们可以计算得出,这个结构体的大小占1+4+1+8+1=15个字节,但是用unsafe.Sizeof计算发现part结构体占32字节,是不是有点惊讶
   463  
   464  这里面就涉及到了内存对齐,下面我们来分析一下:
   465  
   466  ![alignof](../resources/images/alignof.png)
   467  
   468  调整一下 
   469  
   470  ```go
   471  type part struct {
   472  	a bool  // 1
   473  	c int8  // 1
   474  	e byte  // 1
   475  	b int32 //4
   476  	d int64 // 8
   477  }
   478  
   479  func main() {
   480  	var p part
   481  	fmt.Println(unsafe.Sizeof(p)) // 16
   482  }
   483  ```