本文提纲
- go内存对齐介绍
- go内存对齐场景
go内存对齐介绍
为什么需要内存对齐
将内存对齐数据总线的宽度,降低硬件实现复杂度,提升传输的效率。
go内存对齐规则
go内存对齐主要参考因素:
- 数据类型自身的大小,符合类型会参考最大成员的大小
- 硬件平台的机器字长
go内存对齐的规则:
- 起始地址 必须是对齐边界的整数倍
- 结构体整体占用字节数 必须是内存对齐边界的整数倍
可以用个unsafe.Sizeof()获取自身类型大小,可以用unsafe.Alignof()获取内存对齐编辑。
go常见内置类型的大小和对齐边界
go内存对齐场景及举例
go常见的内存对齐场景主要是struct和函数的返参和入参。
函数内的局部变量也会内存对齐,不过会自动优化,不需要额外考虑。
struct
未优化前结构体s1如下:
type s1 struct {
a int8
b int64
c int8
d int32
e int16
}
未优化前结构体s1内存对齐后布局如下:
内存对齐优化后结构体S2:
type s2 struct {
a int8
c int8
e int16
d int32
b int64
}
函数返回参数和入参
go函数的返参和入参内存布局等价于结构体。
- go函数的返参和入参的入栈顺序是先返参后入参,并且都是从右往左。
具体参数入栈等后续函数栈帧出再介绍。 - 调用函数时,参数和返回值看起来更像是先参数后返回值,从左至右的顺序分配,并且是从低地址开始使用的。
函数内局部变量
函数内的局部变量也等价于结构体,但是局部变量在栈帧上会顺序调整,与声明顺序并不一定一致。
举例:
func f() {
var a int8
var b int64
var c int32
var d int16
var e int8
println(&a, &b, &c, &d, &e)
}
输出结果:
0x88387b1 0x88387b8 0x88387b4 0x88387b2 0x88387b0
等价于结构体布局:
type sf struct {
e int8
a int8
d int16
c int32
b int64
}
为什么go编译器会优化局部变量内存布局,而不优化参数和返回值的内存布局呢?
因为函数本身是对代码单元的封装,参数和返回值属于对外暴露的接口,编译器必须按照函数原型来呈现,而局部变量属于封装在内部的参数,不会对外暴露,所以编译器可以优化调整布局,不会对函数意外造成影响。