![aee58044801b846518d4419ce3aee77a.png](https://img-blog.csdnimg.cn/img_convert/aee58044801b846518d4419ce3aee77a.png)
本篇文章介绍Go是如何应用内存对齐,来分配变量空间
先想一个问题,这里有一个结构体T,如何算出该结构体占用的内存大小,以及各个字段在内存如何分布的?Go语言是如何进行内存对齐的?
type T struct {
a bool
b int8
c uint16
d uint32
e int64
f bool
}
要想解答这个问题,要先了解两个概念
类型尺寸
go白皮书只对以下类型占用的内存大小进行了明确规定.
type size in bytes
byte, uint8, int8 1
uint16, int16 2
uint32, int32, float32 4
uint64, int64, float64, complex64 8
complex128 16
uint either 32 or 64 bits
int same size as uint
uintptr an unsigned integer large enough to store the uninterpreted bits of a pointer value
uint和int类型的大小取决于编译器和系统架构,通常32位架构为4字节,64位架构为8字节
uintptr 取决于编译器实现,但必须保证一定能够存下任一个内存地址.
除了上述明确规定的,白皮书没有对其他种类的类型尺寸进行明确规定.
类型尺寸获取代码:
func main(){
var i bool = true
var j int = 40
fmt.Println("bool-size", unsafe.Sizeof(i))
fmt.Println("int-size", unsafe.Sizeof(j))
}
输出结果:
bool-size 1
int-size 8
bool类型的大小占用1字节
int类型的大小占用8字节,因为我是64位系统,如果你是32位的输出会是4
如果仅仅按照类型尺寸的规则,来累加计算结构体T中每个字段的存储大小,发现和实际存储的大小不一样的.因为Go在分配结构体内存的时候,还需要参照对齐保证.
对齐保证
对齐保证也称为值地址对齐保证.也就是在分配变量存储位置的时候,不是随便分配的,是按照对齐保证的整数倍来分配内存地址
go白皮书仅仅规定了对齐保证的基本要求
The following minimal alignment properties are guaranteed:
1. For a variable `x` of any type: `unsafe.Alignof(x)` is at least 1.
2. For a variable `x` of struct type: `unsafe.Alignof(x)` is the largest of all the values `unsafe.Alignof(x.f)` for each field `f` of `x`, but at least 1.
3. For a variable `x` of array type: `unsafe.Alignof(x)` is the same as the alignment of a variable of the array's element type.
翻译版本
1. 对于任何类型的变量x,unsafe.Alignof(x)最小为1
2. 对于结构体类型变量x,unsafe.Alignof(x)的数值是结构体中所有字段的对齐保证中的最大值( the largest ),作为结构体的对齐保证,最小为1.
3. 对于数组类型变量x, unsafe.Alignof(x)的结果和数组元素的类型的对齐保证一致.
Go只是规定了对齐保证的基本规则,但是对于不同编译器不同的架构甚至于同一个编译器的不同版本,实现的对齐保证都会有一定的差异,了解规则即可.
使用的go1.15的标准编译器实现的对齐保证如下:
类型种类 对齐保证(字节数)
------------------------------------------------------
bool, byte, uint8, int8 1
uint16, int16 2
uint32, int32 4
float32, complex64 4
数组 取决于元素类型
结构体类型 取决于各个字段类型
其它类型 一个自然字的尺寸.在32位的架构上为4字节,在64位的架构上为8字节。
每个类型有两个对齐保证.
- 当它被用做结构体类型的字段类型时的对齐保证称为此类型的字段对齐保证,
- 其它情形的对齐保证称为此类型的一般对齐保证。
对齐保证获取代码:
func main(){
var i bool = true
var j int = 40
fmt.Println("bool-align", unsafe.Alignof(i)) //一般对齐保证
fmt.Println("int-align", unsafe.Alignof(j))
type temp struct {
a bool
b int
}
var tmp = temp{}
fmt.Println("a", unsafe.Alignof(tmp.a)) //字段对齐保证
fmt.Println("b", unsafe.Alignof(tmp.b))
}
输出结果:
bool-align 1
int-align 8
a 1
b 8
不管是字段对齐保证还是一般对齐保证,获取的方法都是一样的,不需要刻意区分.
实例应用
内存对齐遵循的规则
变量的存储起始地址一定是对齐保证的整数倍
变量的大小是对齐保证的整数倍.所有的类型都要遵守这一规则.
package main
import (
"fmt"
"unsafe"
)
type T struct {
a bool
b int8
c uint16
d uint32
e int64
f bool
}
func main(){
var t = T{}
fmt.Println("t占用的实际内存大小:", unsafe.Sizeof(t), "字节,结构体对齐保证:", unsafe.Alignof(t))
fmt.Println("a:", unsafe.Sizeof(t.a), "字节,字段对齐保证:", unsafe.Alignof(t.a), ",偏移地址:", unsafe.Offsetof(t.a))
fmt.Println("b:", unsafe.Sizeof(t.b), "字节,字段对齐保证:", unsafe.Alignof(t.b), ",偏移地址:", unsafe.Offsetof(t.b))
fmt.Println("c:", unsafe.Sizeof(t.c), "字节,字段对齐保证:", unsafe.Alignof(t.c), ",偏移地址:", unsafe.Offsetof(t.c))
fmt.Println("d:", unsafe.Sizeof(t.d), "字节,字段对齐保证:", unsafe.Alignof(t.d), ",偏移地址:", unsafe.Offsetof(t.d))
fmt.Println("e:", unsafe.Sizeof(t.e), "字节,字段对齐保证:", unsafe.Alignof(t.e), ",偏移地址:", unsafe.Offsetof(t.e))
fmt.Println("f:", unsafe.Sizeof(t.f), "字节,字段对齐保证:", unsafe.Alignof(t.f), ",偏移地址:", unsafe.Offsetof(t.f))
fmt.Println(uintptr(unsafe.Pointer(&t)))
}
输出结果:
t占用的实际内存大小: 24 字节,结构体对齐保证: 8
a: 1 字节,字段对齐保证: 1 ,偏移地址: 0
b: 1 字节,字段对齐保证: 1 ,偏移地址: 1
c: 2 字节,字段对齐保证: 2 ,偏移地址: 2
d: 4 字节,字段对齐保证: 4 ,偏移地址: 4
e: 8 字节,字段对齐保证: 8 ,偏移地址: 8
f: 1 字节,字段对齐保证: 1 ,偏移地址: 16
824634378592
如果按照上面输出的字段大小累加(1+1+2+4+8+1 = 17字节)明显和实际内存大小24字节不符合.现在来一步步解析上面的结果是怎么出来的.
![53bc9367222e2f1a16d04d28ad2eefac.png](https://img-blog.csdnimg.cn/img_convert/53bc9367222e2f1a16d04d28ad2eefac.png)
以此类推
![592cdc15d943b7ee8b0e8e921fb79239.png](https://img-blog.csdnimg.cn/img_convert/592cdc15d943b7ee8b0e8e921fb79239.png)
最后一个变量f完美分配完成,但是这个时候整个结构体类型还需要满足一个条件,类型的大小需要是对齐保证的整数倍.
结构体的对齐保证是所有字段对齐保证中的最大值,这里为8.而分配的内存大小只有17字节,不满足整数倍的关系.需要填充7字节,保证这个关系
![b5324fcb7662b0312edf897555276fb7.png](https://img-blog.csdnimg.cn/img_convert/b5324fcb7662b0312edf897555276fb7.png)
最终结构体t的内存分配完成为24字节,符合我们的打印输出
结构体在内存中完整的存储图示:
![d4ef1ced288cba7430b17b159f7c833e.png](https://img-blog.csdnimg.cn/img_convert/d4ef1ced288cba7430b17b159f7c833e.png)
举一反三:这里把结构体T中c和d字段的位置替换下,定义结构体R,再来验证下上面分析规则.
package main
import (
"fmt"
"unsafe"
)
type R struct {
a bool
b int8
d uint32
c uint16
e int64
f bool
}
func main(){
var r = R{}
fmt.Println("r占用的实际内存大小:", unsafe.Sizeof(r), "字节,结构体对齐保证:", unsafe.Alignof(r))
fmt.Println("a:", unsafe.Sizeof(r.a), "字节,字段对齐保证:", unsafe.Alignof(r.a), ",偏移地址:", unsafe.Offsetof(r.a))
fmt.Println("b:", unsafe.Sizeof(r.b), "字节,字段对齐保证:", unsafe.Alignof(r.b), ",偏移地址:", unsafe.Offsetof(r.b))
fmt.Println("d:", unsafe.Sizeof(r.d), "字节,字段对齐保证:", unsafe.Alignof(r.d), ",偏移地址:", unsafe.Offsetof(r.d))
fmt.Println("c:", unsafe.Sizeof(r.c), "字节,字段对齐保证:", unsafe.Alignof(r.c), ",偏移地址:", unsafe.Offsetof(r.c))
fmt.Println("e:", unsafe.Sizeof(r.e), "字节,字段对齐保证:", unsafe.Alignof(t.e), ",偏移地址:", unsafe.Offsetof(r.e))
fmt.Println("f:", unsafe.Sizeof(r.f), "字节,字段对齐保证:", unsafe.Alignof(r.f), ",偏移地址:", unsafe.Offsetof(r.f))
fmt.Println(uintptr(unsafe.Pointer(&r)))
}
输出结果:
r占用的实际内存大小: 32 字节,结构体对齐保证: 8
a: 1 字节,字段对齐保证: 1 ,偏移地址: 0
b: 1 字节,字段对齐保证: 1 ,偏移地址: 1
d: 4 字节,字段对齐保证: 4 ,偏移地址: 4
c: 2 字节,字段对齐保证: 2 ,偏移地址: 8
e: 8 字节,字段对齐保证: 8 ,偏移地址: 16
f: 1 字节,字段对齐保证: 1 ,偏移地址: 24
824634378616
![32d0e9a5a13c14b3a5b5726b324a2434.png](https://img-blog.csdnimg.cn/img_convert/32d0e9a5a13c14b3a5b5726b324a2434.png)
我们发现相同的字段数量和类型的结构体,字段位置的不同结构体T(24字节)和R(32字节)在内存中的实际存储大小是不同的.
在构造结构体时候,占用较大存储空间的字段后写比先写,占用的空间更小.
总结
这里主要是验证内存对齐规则对实际存储空间的影响,并验证了内存对齐的规则.平时工作中不需要关心值地址的对齐保证,编译器已经自动完成了相关的工作.除非打算优化下内存消耗.特别是定义结构体时,可以参照下上面的结论.
源码地址:
https://github.com/gofish2020/zeus/tree/main/source/memoryalign
参考资料:
https://golang.google.cn/ref/spec#Size_and_alignment_guarantees
https://golang.google.cn/ref/spec#Numeric_types