背景
很多高性能的开发都要抠细节,其中Golang日常开发需要注意的性能细节就有内存对齐,所以记录一下,分享给大家。
结构体的大小
计算一个变量类型的内存占用大小可以使用unsafe.Sizeof()
函数。所有的标准变量所占的内存大小都是固定的,但是我们日常开发中自定义的结构体占内存多大,我们真的了解吗?
我们一起看下面的代码:
package main
import (
"fmt"
"unsafe"
)
// 16 个字节
type Demo_1 struct {
num1 int // 8 个字节
num2 int // 8 个字节
}
// 4 个字节
type Demo_2 struct {
num1 int8 // 1 个字节
num2 int16 // 2 个字节
}
func main() {
var (
num1 int // 8 个字节
num2 int8 // 1 个字节
num3 int16 // 2 个字节
d1 Demo_1 // 16 个字节
d2 Demo_2 // 4 个字节
)
// 打印 int、 int8、int16 占用的内存大小: 8, 1, 2
fmt.Println("int sizeof:\t", unsafe.Sizeof(num1), "\tint8 sizeof:\t", unsafe.Sizeof(num2), "\tint16 sizeof:\t", unsafe.Sizeof(num3))
// 打印两种结构体占用的内存大小: 16, 5
fmt.Println("demo_1 sizeof:\t", unsafe.Sizeof(d1), "\tdemo_2 sizeof:\t", unsafe.Sizeof(d2))
}
在64位机器运行输出结果:
int sizeof: 8 int8 sizeof: 1 int16 sizeof: 2
demo_1 sizeof: 16 demo_2 sizeof: 4
我们可以看到:
Demo_1
包含了两个int
类型、在64位机器中都是占用8个字节,所以结构体总的占用内存就是8+8=16字节。Demo_2
包含了int8
、int16
两种,按照上面的分析,结构体所占用的内存应该为1+2=3,但是输出的结果却是4,多出来一个字节,原因就是内存对齐,所以一个结构体的长度 = 各变量的长度 + 内存对齐的大小
内存对齐
内存对齐的原因:cpu 访问内存是字长(一个字的长度)为单位进行读取,比如:32位的cpu字长是4字节,那么cpu访问内存的大小也是4个字节。
内存对齐的目的:减少cpu读取内存的频率,增大吞吐量。
同样是读取8个字节的数据,一次读取字节长为4个字节,则只需要读取2次即可,若不进行字节对齐,则有可能增加cpu访问内存的次数,例子如下:
如图所示:
- 无内存对齐时,访问变量A时,读取1次;访问变量B时,第一次读取到第一个字节,第二次读取到后两个字节,共读取2次。
- 有内存对齐时,访问变量A、B,都只读取1次。
另外内存对齐之后,如果变量的大小不超过字长,每次原子访问对变量来说也是原子操作,有利于高并发场景下的性能提升。
获取内存对齐倍数
在前面的例子中Demo_2
结构体理应占据 3 个字节,但是内存对齐的结果是占用了 4 个字节,在 Go 语音中的内存对齐规律,我们可以用unsafe.Alignof()
函数来获取一个变量的内存对齐倍数。
unsafe.Alignof(Demo_1{}) // 8
unsafe.Alignof(Demo_2{}) // 2
Demo_1
对齐倍数是 8 个字节,两个变量占 16 个字节,无需额外的空间对齐。Demo_2
对齐倍数是 2 个字节,两个变量占据 3 个字节,内存对齐后占用 4 个字节,才是 2 的倍数。
开发中的技巧
合理的定义顺序
先来看看一个例子,int8
,int16
, int32
的定义顺序带来的内存占用大小。
package main
import (
"fmt"
"unsafe"
)
// 8 个字节
type Demo_1 struct {
num1 int8 // 1 个字节
num2 int16 // 2 个字节
num3 int32 // 4 个字节
}
// 12 个字节
type Demo_2 struct {
num1 int8 // 1 个字节
num3 int32 // 4 个字节
num2 int16 // 2 个字节
}
func main() {
var (
d1 Demo_1
d2 Demo_2
)
fmt.Println("demo_1 sizeof:\t", unsafe.Sizeof(d1), "\tdemo_2 sizeof:\t", unsafe.Sizeof(d2))
fmt.Println("demo_1 alignof:\t", unsafe.Alignof(d2), "\tdemo_2 alignof:\t", unsafe.Alignof(d2))
}
运行结果:
demo_1 sizeof: 8 demo_2 sizeof: 12
demo_1 alignof: 4 demo_2 alignof: 4
可以看到,每个变量根据内存对齐倍数来确定在内存中的偏移量,顺序不同上一个字段因对齐浪费的内存大小也不同。我们逐个分析:
Demo_1
的对齐方式:
- 第一个变量 num1 默认已经对齐,从位置 0 开始,占 1 个字节。
- 第二个变量 num2 对齐倍数为 2,所以需要num1 之后空出一个字节,偏移量才是 2 的倍数。
- 第三个变量 num3 对齐倍数为 4,从偏移量为 4 的位置开始读取,已经对齐了,无额外对齐浪费。
Demo_2
的对齐方式
- 第一个变量 num1 默认对齐,从位置 0 开始,占用了 1 个字节。
- 第二个变量 num3 对齐倍数为 4,所以需要 num1 之后空出 3 个字节,偏移量才是 4 的倍数。
- 第三个变量 num2 对齐倍数为 2,num2 之后的偏移量是 2 的倍数,占用 2 个字节。
由于 num2 的对齐倍数是由 num3 决定的,所以内存对齐需要额外的 2 个字节。
所以,在对内存比较敏感的结构,我们可以通过合理定义变量的顺序来节省内存。
空结构体的对齐
在 Go 中,空结构体struct{}
是个特殊的存在,它的内存占用大小为 0。可用于集合、协程中控制 channel 的值、方法的定义,相比于传bool
更节省内存(非本文重点)。
var empty struct{}
fmt.Println(unsafe.Alignof(empty), unsafe.Sizeof(empty))
// 运行结果
// 1 0
但在结构体中,定义在头和尾处则会有两种不同的效果,如下:
// 16 个字节
type Demo_1 struct {
empty struct{} // 0 个字节
num1 int8 // 1 个字节
num2 int // 8 个字节
}
// 16 个字节
type Demo_2 struct {
num1 int8 // 1 个字节
empty struct{} // 0 个字节
num2 int // 8 个字节
}
// 24 个字节
type Demo_3 struct {
num1 int8 // 1 个字节
num2 int // 8 个字节 int16 2 个字节
empty struct{} // 8 个字节 2 个字节
}
如果空结构体定义在结构体的头部和中间,内存对齐时不会占用额外的内存开销。反之,若定义在最后一个字段,则空结构体的占用的大小为内存对齐倍数的大小。
这么做的原因:若有指针指向了Demo_3.empty
,返回的地址则在结构体之外,如果该指针指向的内存一直不释放,容易造成内存泄露(内存在结构体之外,不会因为结构体的释放而释放)。
func main() {
var (
d1 Demo_1 // 16 个字节
d2 Demo_2 // 4 个字节
d3 Demo_3 // 24 个字节
empty_1 struct{}
empty_2 struct{}
)
ptr_d2 := uintptr(unsafe.Pointer(&d2.empty)) - uintptr(unsafe.Pointer(&d2))
ptr_d3 := uintptr(unsafe.Pointer(&d3.empty)) - uintptr(unsafe.Pointer(&d3))
fmt.Printf("empty_1 postion: [%p], empty_2 postion: %p\n", &empty_1, &empty_2)
fmt.Printf("demo_1 sizeof %d, demo_1 postion: [%p], empty postion: %p\n", unsafe.Sizeof(d1), &d1, &(d1.empty))
fmt.Printf("demo_2 sizeof %d, demo_2 postion: [%p], empty postion: %p, diff bit: %d\n", unsafe.Sizeof(d2), &d2, &(d2.empty), ptr_d2)
fmt.Printf("demo_3 sizeof %d, demo_3 postion: [%p], empty postion: %p, diff bit: %d\n", unsafe.Sizeof(d3), &d3, &(d3.empty), ptr_d3)
}
运行结果:
empty_1 postion: [0x1171f80], empty_2 postion: 0x1171f80
demo_1 sizeof 16, demo_1 postion: [0xc0000ac010], empty postion: 0xc0000ac010
demo_2 sizeof 16, demo_2 postion: [0xc0000ac020], empty postion: 0xc0000ac021, diff bit: 1
demo_3 sizeof 24, demo_3 postion: [0xc0000b4018], empty postion: 0xc0000b4028, diff bit: 16
总结
在 go 中定义结构体尽量先定义内存占用比较小的变量,避免在内存对齐中浪费多余的内存,行程良好的编程习惯。
本文由 Chakhsu Lau 创作,采用 知识共享署名4.0 国际许可协议进行许可。
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。