版权声明: 本文为 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++的野指针"
字(word):
是用于表示其自然的数据单位,也叫machine word
。字是电脑用来一次性处理事务的一个固定长度。
字长:
一个字的位数,现代电脑的字长通常为 16、32、64 位。(一般 N 位系统的字长是 N/8
字节。)
操作系统并非一个字节一个字节访问内存,而是按2, 4, 8
这样的字长来访问。因此,当CPU从存储器读数据到寄存器,或者从寄存器写数据到存储器,IO的数据长度通常是字长。如 32 位系统访问粒度是 4 字节(bytes),64 位系统的是 8 字节。
当被访问的数据长度为 n
字节且该数据地址为n
字节对齐,那么操作系统就可以高效地一次定位到数据,无需多次读取、处理对齐运算等额外操作。
数据结构应该尽可能地在自然边界上对齐。如果访问未对齐的内存,CPU需要做两次内存访问。
对于go数据类型的大小保证和对齐保证,官方文档如下图
翻译圈红的内容:如果结构或数组类型包含大小为零的字段(或元素),则其大小为零。 两个不同的零大小变量在内存中可能具有相同的地址。
大小保证
在go中,如果两个值的类型为同一种类的类型,并且他们的类型的种类不为接口
,数组
和结构体
,则这连个值的尺寸总是相等的
目前(go1.14),至少对于官方标准编译器来说,任何一个特定类型的所有值的尺寸都是相等的,所以我们也常说一个值的尺寸为此值的类型的尺寸
下表列出了各种种类的类型的尺寸(对标准编译器1.14来说):
一个结构体类型的尺寸取决于它的各个字段的类型尺寸和这些字段的排列顺序,为了程序执行性能,编译器需要保证某些类型的值在内存中存放时必须满足特定的内存地址对齐要求
。地址对齐可能会造成相邻的两个字段之间在内存中被插入填充一些多余的字节,所以,一个结构体类型的尺寸比定不小于(常常会大于)此结构体类型的各个字段的类型尺寸之和。
一个数组类型的尺寸取决于它的元素类型的尺寸和它的长度,它的尺寸为它的元素类型的尺寸和它的长度的乘积。
Struct{}和[0]T{}的大小为0;不同类型的大小为0的变量可能指向同一快地址。
对齐保证
官方文档中的对齐保证的要求只有如下解释谷歌翻译结果:(原文就是圈红部分的上面的三条)
类型对齐保证也称为值地址对齐保证
。如果一个类型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{}) 会是多少呢?
// 大小不变
// 连续声明两个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
每个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,就没有出现填充对齐呢?