go面试3(内存对齐)

go面试3(内存对齐)

版权声明: 本文为 InfoQ 作者【PONPON】的原创文章。

原文链接:【https://xie.infoq.cn/article/594a7f54c639accb53796cfc7】。

本文是在拜读原文后,将原文出现的面试题加入自己的思考,做了一些转变,也补充了自己一些疑惑,剩余的知识点和原理跟原文一致,原文作者的编程思想之深令人佩服,深究语言源码的精神值得学习。

一、面试题

type S struct {
     
	A uint8 
	B uint32 
	C int16 
	D int64 
	E [2]string 
	F struct{
     } 
}

上面结构体所占内存是多少?(cpu为64位)

fun main(){
     
	fmt.Println(unsafe.Offsetof(S{
     }.B)) // 4
	fmt.Println(unsafe.Offsetof(S{
     }.C)) // 8
	fmt.Println(unsafe.Offsetof(S{
     }.D)) // 16
	fmt.Println(unsafe.Offsetof(S{
     }.E)) // 24
  fmt.Println(unsafe.Offsetof(S{
     }.F)) // 56
  fmt.Println(unsafe.Sizeof(S{
     }.F)) // 0
	fmt.Println(unsafe.Sizeof(S{
     })) // 64
}

二、分析

该面试题考察goland的<内存对齐>,从S的结构上看是8bytes对齐的,一般的回答可能是56bytes,因为从打印的S.F的偏移量看,是56byte,并且S.E的大小是0,但是最终的S却是64,所以必然有隐性的8bytes的padding,
为什么有这个padding,github上的大佬给出的解释是:"结构体尾部的size为0的变量(字段)会被分配内存空间进行填充,
如果不这么处理,改变量(字段)指针将会指向一个非法的内存空间,类似c/c++的野指针"

三、原理

1、术语

  • 字(word):

    是用于表示其自然的数据单位,也叫machine word。字是电脑用来一次性处理事务的一个固定长度。

  • 字长:

    一个字的位数,现代电脑的字长通常为 16、32、64 位。(一般 N 位系统的字长是 N/8 字节。)

2. 为什么要对齐

操作系统并非一个字节一个字节访问内存,而是按2, 4, 8这样的字长来访问。因此,当CPU从存储器读数据到寄存器,或者从寄存器写数据到存储器,IO的数据长度通常是字长。如 32 位系统访问粒度是 4 字节(bytes),64 位系统的是 8 字节。

当被访问的数据长度为 n 字节且该数据地址为n字节对齐,那么操作系统就可以高效地一次定位到数据,无需多次读取、处理对齐运算等额外操作。

数据结构应该尽可能地在自然边界上对齐。如果访问未对齐的内存,CPU需要做两次内存访问。

3、数据结构对齐

对于go数据类型的大小保证和对齐保证,官方文档如下图

go面试3(内存对齐)_第1张图片

翻译圈红的内容:如果结构或数组类型包含大小为零的字段(或元素),则其大小为零。 两个不同的零大小变量在内存中可能具有相同的地址。

大小保证

在go中,如果两个值的类型为同一种类的类型,并且他们的类型的种类不为接口数组结构体,则这连个值的尺寸总是相等的

目前(go1.14),至少对于官方标准编译器来说,任何一个特定类型的所有值的尺寸都是相等的,所以我们也常说一个值的尺寸为此值的类型的尺寸

下表列出了各种种类的类型的尺寸(对标准编译器1.14来说):

go面试3(内存对齐)_第2张图片

一个结构体类型的尺寸取决于它的各个字段的类型尺寸和这些字段的排列顺序,为了程序执行性能,编译器需要保证某些类型的值在内存中存放时必须满足特定的内存地址对齐要求。地址对齐可能会造成相邻的两个字段之间在内存中被插入填充一些多余的字节,所以,一个结构体类型的尺寸比定不小于(常常会大于)此结构体类型的各个字段的类型尺寸之和。

一个数组类型的尺寸取决于它的元素类型的尺寸和它的长度,它的尺寸为它的元素类型的尺寸和它的长度的乘积。

Struct{}和[0]T{}的大小为0;不同类型的大小为0的变量可能指向同一快地址。

对齐保证

官方文档中的对齐保证的要求只有如下解释谷歌翻译结果:(原文就是圈红部分的上面的三条)

  1. 对于任何类型的变量x,unsafe.Alignof(x)的结果最小为1;
  2. 对于结构类型的变量x:对于x的每个字段f,unsafe.Alignof(x)是所有值unsafe.Alignof(x.f)中的最大值,但至少为1。
  3. 对于数组类型的变量x:unsafe.Alignof(x)与数组元素类型的变量的对齐方式相同

go面试3(内存对齐)_第3张图片

类型对齐保证也称为值地址对齐保证。如果一个类型T的对齐保证为N(一个正整数),则在运行时刻T类型的每个(可寻址的)值的地址都是N的倍数。我们也可以说类型T的值的地址保证为N字节对齐的。

事实上,每个类型有两个对齐保证,当它被用作结构体类型的字段类型时的对齐保证为次类型的字段对齐保证,其他情形的对齐保证称为此类型的一般对齐保证

对于一个类型T,我们可以调用unsafe.Alignof(t)来获得它的一般对齐保证,其中t为一个T类型的非字段值,也可以调用unsafe.Alignof(x.t)来获取T的字段对齐摆正,其中X为一个结构体质并且t为一个类型为T的结构体字段值。

在运行时刻,对于类型为T的一个值t,我们可以调用reflect.Typeof(t).Align()来获得类型T的一般对齐保证,也可以调用reflect.TypeOf(t).FieldAlign()来获得T的字段对齐保证

对于当前的官方go编译器(1.14版本),一个类型的一般对齐保证和字段对齐保证总是相等的。

重排优化

针对开头的面试体进行优化,减少内存占用

type S1 struct {
     
	A uint8 // 1 补3为4
	B uint32 // 4   A+B=8
	C int16 //2 补6为8
	D int64 //8
	E [2]string // 16*2=32 
	F struct{
     }  //8
}

type S2 struct {
     
	A uint8 // 1 补1为2
	B int16 //2  
	C uint32 //4 A+B+C=8
	D struct{
     } //  0
	E int64 //  8
	F [2]string // 16*2

}
fmt.Printf("S2 sizeof: %d,alignof:%d\n",unsafe.Sizeof(S2{
     }),unsafe.Alignof(Sc{
     }))
	fmt.Printf("S1 sizeof: %d,alignof:%d\n",unsafe.Sizeof(S1{
     }),unsafe.Alignof(Sb{
     }))

/*
S2 sizeof: 48,alignof:8
S1 sizeof: 64,alignof:8
*/

分析:S1和S2内字段的最大的都是int64,大小伟8bytes,对齐按机器字确定,64位下位 8Bytes,所以按8Bytes对齐

合理重排字段可以减少填充,是struct字段排列更紧密

零大小字段对齐

零大小字段(zero size field)是指struct{},大小为0,按理作为字段时不需要对齐,但当在作为结构体最后一个字段(final field)时需要对齐的。即开头的面试题,架设有指针指向这个final zero field,返回的地址将在结构体之外(即指向了别的内存),如果此指针一直存活不释放对应的内存,就会有内存泄漏的问题(该内存不因结构体释放而释放),go会对这种finam zero field也做填充,使之对齐。当然,有一种情况不需要对这个final zero field做额外填充,也就是这个末尾的上一个字段对齐,需要对这个字段进行填充时,final zero field就不需要再次填充,而是直接利用上一个字段的填充


type Sa struct {
      //24
	A uint64 //8
	B uint32 //4
	C int32 //4
	D int8 //1
	E struct{
     }
}
fmt.Printf("Sa sizeof: %d,alignof:%d\n",unsafe.Sizeof(Sa{
     }),unsafe.Alignof(Sa{
     }))

//  假如这个 E 不是struct{},而是 [0]int32 或者 [0]int64,那 unsafe.Sizeof(S{}) 会是多少呢? 
// 大小不变

四、总结

  • 内存对齐是为了让cpu更高效的访问内存中的数据
  • struct的对齐是:如果类型t的对齐保证是n,那么类型t的每个值的地址在运行时必须是n的倍数
  • struct内字段如果填充过多,可以尝试重排,使字段排列更紧密,减少内存浪费
  • 零大小字段要避免作为struct最后一个字段,会有内存浪费
  • 32位系统上对64位字的原子访问要保证其是8bytes对齐的;当让如果不必要的话,还是用加锁(mutex)的方式更清晰简单

五、面试题拓展

// 连续声明两个Sa的变量s1,s2,那么s2的内存地址是怎样的?
type Sa struct {
      //24
	A uint64 //8
	B uint32 //4
	C int32 //4
	D int8 //1
	E struct{
     }
}
var s1,s2 Sa
fmt.Printf("S1.E offsetof:%d,s1.E sizeof:%d, s1 sizeof:%d, s1.A address:%v",unsafe.Offsetof(s1.E),unsafe.Sizeof(s1.E),unsafe.Sizeof(s1), &s1.A)

fmt.Printf("S1.E offsetof:%d,s1.E sizeof:%d, s1 sizeof:%d, s1.A address:%v\n", unsafe.Offsetof(s2.E),
		unsafe.Sizeof(s2.E), unsafe.Sizeof(s2), &s2.A)
ptr1 := uintptr(unsafe.Pointer(&s1))
ptr2 := uintptr(unsafe.Pointer(&s2))
fmt.Println(ptr2-ptr1) //32
/*
S1.E offsetof:17,s1.E sizeof:0, s1 sizeof:24, s1.A address:0x11a6cd0
S1.E offsetof:17,s1.E sizeof:0, s1 sizeof:24, s1.A address:0x11a6cf0
*/

因为s1和s2在内存上是连续的,我们也知道s1的内存是24,那么问什么s2的地址相对s1的地址偏移量却是32呢?(0x11a6cf0-0x11a6cd0=32)

原因是

因为对于s1这个对象,它的大小是24bytes,而go在内存分配时,会冲span种拿大于或等于40的最小的span中的一个快给这个对象,二sizeclass中这个快的大小值为32,所以虽然s1的大小是24bytes,但是实际分配给这个对象的内存大小是48

补充知识点:

go的内存分配,首先是按照sizeclass划分span,然后每个span中的page又分成一个个小格子(大小相同的对象object)

span是golang内存管理的基本单位,是由一片连续的8kb(golfing page的大小)的页组成的大块内存

如图,span由一组连续的页组成,按照一定大小划分成object

go面试3(内存对齐)_第4张图片

每个span管理指定规格(golang中的page为单位)的内存快,内存池分配出不同规格的内存快就是通过span体现出来的,应用程序创建对象就是通过找到对应规格的span来存储的,下面是mspan结构中的主要部分。

type mspan struct {
     
	next *mspan     // next span in list, or nil if none
	prev *mspan     // previous span in list, or nil if none
	list *mSpanList // For debugging. TODO: Remove.

	startAddr uintptr // address of first byte of span aka s.base()
	npages    uintptr // number of pages in span

	manualFreeList gclinkptr // list of free objects in mSpanManual spans

	
	freeindex uintptr
	
	nelems uintptr // number of object in the span.

	
	allocCache uint64

	allocBits  *gcBits
	gcmarkBits *gcBits


	sweepgen    uint32
	divMul      uint16        // for divide by elemsize - divMagic.mul
	baseMask    uint16        // if non-0, elemsize is a power of 2, & this will get object allocation base
	allocCount  uint16        // number of allocated objects
	spanclass   spanClass     // size class and noscan (uint8)
	state       mSpanStateBox // mSpanInUse etc; accessed atomically (get/set methods)
	needzero    uint8         // needs to be zeroed before allocation
	divShift    uint8         // for divide by elemsize - divMagic.shift
	divShift2   uint8         // for divide by elemsize - divMagic.shift2
	elemsize    uintptr       // computed from sizeclass or from npages
	limit       uintptr       // end of data in span
	speciallock mutex         // guards specials list
	specials    *special      // linked list of special records sorted by offset.
}

那么要想区分不同规格的span,必须要有一个标识,每个span通过spanclass标识属于哪种规格的span,golang的span规格一共有67种,具体如下

// Code generated by mksizeclasses.go; DO NOT EDIT.
//go:generate go run mksizeclasses.go

package runtime

// class  bytes/obj  bytes/span  objects  tail waste  max waste
//     1          8        8192     1024           0     87.50%
//     2         16        8192      512           0     43.75%
//     3         32        8192      256           0     46.88%
//     4         48        8192      170          32     31.52%
//     5         64        8192      128           0     23.44%
//     6         80        8192      102          32     19.07%
//     7         96        8192       85          32     15.95%
//     8        112        8192       73          16     13.56%
//     9        128        8192       64           0     11.72%
//    10        144        8192       56         128     11.82%
//    11        160        8192       51          32      9.73%
//    12        176        8192       46          96      9.59%
//    13        192        8192       42         128      9.25%
//    14        208        8192       39          80      8.12%
//    15        224        8192       36         128      8.15%
//    16        240        8192       34          32      6.62%
//    17        256        8192       32           0      5.86%
//    18        288        8192       28         128     12.16%
//    19        320        8192       25         192     11.80%
//    20        352        8192       23          96      9.88%
//    21        384        8192       21         128      9.51%
//    22        416        8192       19         288     10.71%
//    23        448        8192       18         128      8.37%
//    24        480        8192       17          32      6.82%
//    25        512        8192       16           0      6.05%
//    26        576        8192       14         128     12.33%
//    27        640        8192       12         512     15.48%
//    28        704        8192       11         448     13.93%
//    29        768        8192       10         512     13.94%
//    30        896        8192        9         128     15.52%
//    31       1024        8192        8           0     12.40%
//    32       1152        8192        7         128     12.41%
//    33       1280        8192        6         512     15.55%
//    34       1408       16384       11         896     14.00%
//    35       1536        8192        5         512     14.00%
//    36       1792       16384        9         256     15.57%
//    37       2048        8192        4           0     12.45%
//    38       2304       16384        7         256     12.46%
//    39       2688        8192        3         128     15.59%
//    40       3072       24576        8           0     12.47%
//    41       3200       16384        5         384      6.22%
//    42       3456       24576        7         384      8.83%
//    43       4096        8192        2           0     15.60%
//    44       4864       24576        5         256     16.65%
//    45       5376       16384        3         256     10.92%
//    46       6144       24576        4           0     12.48%
//    47       6528       32768        5         128      6.23%
//    48       6784       40960        6         256      4.36%
//    49       6912       49152        7         768      3.37%
//    50       8192        8192        1           0     15.61%
//    51       9472       57344        6         512     14.28%
//    52       9728       49152        5         512      3.64%
//    53      10240       40960        4           0      4.99%
//    54      10880       32768        3         128      6.24%
//    55      12288       24576        2           0     11.45%
//    56      13568       40960        3         256      9.99%
//    57      14336       57344        4           0      5.35%
//    58      16384       16384        1           0     12.49%
//    59      18432       73728        4           0     11.11%
//    60      19072       57344        3         128      3.57%
//    61      20480       40960        2           0      6.87%
//    62      21760       65536        3         256      6.25%
//    63      24576       24576        1           0     11.45%
//    64      27264       81920        3         128     10.00%
//    65      28672       57344        2           0      4.91%
//    66      32768       32768        1           0     12.50%

const (
	_MaxSmallSize   = 32768
	smallSizeDiv    = 8
	smallSizeMax    = 1024
	largeSizeDiv    = 128
	_NumSizeClasses = 67
	_PageShift      = 13
)

var class_to_size = [_NumSizeClasses]uint16{
     0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}
var class_to_allocnpages = [_NumSizeClasses]uint8{
     0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 3, 2, 3, 1, 3, 2, 3, 4, 5, 6, 1, 7, 6, 5, 4, 3, 5, 7, 2, 9, 7, 5, 8, 3, 10, 7, 4}

type divMagic struct {
     
	shift    uint8
	shift2   uint8
	mul      uint16
	baseMask uint16
}

var class_to_divmagic = [_NumSizeClasses]divMagic{
     {
     0, 0, 0, 0}, {
     3, 0, 1, 65528}, {
     4, 0, 1, 65520}, {
     5, 0, 1, 65504}, {
     4, 11, 683, 0}, {
     6, 0, 1, 65472}, {
     4, 10, 205, 0}, {
     5, 9, 171, 0}, {
     4, 11, 293, 0}, {
     7, 0, 1, 65408}, {
     4, 13, 911, 0}, {
     5, 10, 205, 0}, {
     4, 12, 373, 0}, {
     6, 9, 171, 0}, {
     4, 13, 631, 0}, {
     5, 11, 293, 0}, {
     4, 13, 547, 0}, {
     8, 0, 1, 65280}, {
     5, 9, 57, 0}, {
     6, 9, 103, 0}, {
     5, 12, 373, 0}, {
     7, 7, 43, 0}, {
     5, 10, 79, 0}, {
     6, 10, 147, 0}, {
     5, 11, 137, 0}, {
     9, 0, 1, 65024}, {
     6, 9, 57, 0}, {
     7, 9, 103, 0}, {
     6, 11, 187, 0}, {
     8, 7, 43, 0}, {
     7, 8, 37, 0}, {
     10, 0, 1, 64512}, {
     7, 9, 57, 0}, {
     8, 6, 13, 0}, {
     7, 11, 187, 0}, {
     9, 5, 11, 0}, {
     8, 8, 37, 0}, {
     11, 0, 1, 63488}, {
     8, 9, 57, 0}, {
     7, 10, 49, 0}, {
     10, 5, 11, 0}, {
     7, 10, 41, 0}, {
     7, 9, 19, 0}, {
     12, 0, 1, 61440}, {
     8, 9, 27, 0}, {
     8, 10, 49, 0}, {
     11, 5, 11, 0}, {
     7, 13, 161, 0}, {
     7, 13, 155, 0}, {
     8, 9, 19, 0}, {
     13, 0, 1, 57344}, {
     8, 12, 111, 0}, {
     9, 9, 27, 0}, {
     11, 6, 13, 0}, {
     7, 14, 193, 0}, {
     12, 3, 3, 0}, {
     8, 13, 155, 0}, {
     11, 8, 37, 0}, {
     14, 0, 1, 49152}, {
     11, 8, 29, 0}, {
     7, 13, 55, 0}, {
     12, 5, 7, 0}, {
     8, 14, 193, 0}, {
     13, 3, 3, 0}, {
     7, 14, 77, 0}, {
     12, 7, 19, 0}, {
     15, 0, 1, 32768}}
var size_to_class8 = [smallSizeMax/smallSizeDiv + 1]uint8{
     0, 1, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 18, 18, 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31}
var size_to_class128 = [(_MaxSmallSize-smallSizeMax)/largeSizeDiv + 1]uint8{
     31, 32, 33, 34, 35, 36, 36, 37, 37, 38, 38, 39, 39, 39, 40, 40, 40, 41, 42, 42, 43, 43, 43, 43, 43, 44, 44, 44, 44, 44, 44, 45, 45, 45, 45, 46, 46, 46, 46, 46, 46, 47, 47, 47, 48, 48, 49, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 52, 52, 53, 53, 53, 53, 54, 54, 54, 54, 54, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 57, 57, 57, 57, 57, 57, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 60, 60, 60, 60, 60, 61, 61, 61, 61, 61, 61, 61, 61, 61, 61, 61, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66}

每个mspan按照它自身的属性size class的大小分割成若干个object,每个object可存储一个对西那个,并且会使用一个位图来标记其尚未使用的object。属性seze class决定object大小,而mspan只会分配给和object尺寸大小接近的对象。但让,对象的大小要小于object的大小

疑惑:

type T1 struct {
     
	//24
	A uint8  //1
	B uint16 //2
	E uint8  //1
	C uint32 //4
	D uint64 //8
}
type T2 struct {
     
	// 16
	A uint8  //1
	E uint8  //1
	B uint16 //2
	C uint32 //4 
	D uint64 //8
}

T1 sizeof: 24,alignof:8
T2 sizeof: 16,alignof:8

为什么T1的T1.A, T1.B, T1.E, T1.C 合为8bytes但是还进行了填充对齐呢,而tT2将T2.A, T2.B, T2.E, T2.C四个的内存偏移正好是8bytes,就没有出现填充对齐呢?

你可能感兴趣的:(go)