Intro
1 这是什么玩意?
测试环境的字长为8byte
首先,引入一个很简单的例子
type A struct {
num1 int32
num2 float64
}
type B struct {
num1 int64
num2 float64
}
func main() {
fmt.Println(unsafe.Sizeof(A{}))
fmt.Println(unsafe.Sizeof(B{}))
}
似乎答案很明显,A的大小应该是4 + 8 = 12,而B的大小应该是8 + 8 = 16。然而,输出结果是两个16。这时候就是发生了内存对齐,导致了struct占用的内存比想象中大。
2 为什么要内存对齐
首先,必须引入字长 word size的概念,简单地说就是CPU一次性处理的数据长度。字长为32位的CPU就叫做32位CPU。字长影响的内容较多,在这不做过多的讨论,只要了解上述的概念就足够往下学习了。
操作系统访问内存并非逐字节进行访问的,而是逐字长的,也就是说,每次访问内存,都是从字长的整数倍的地址开始的
让我们看看下面这种情况(图片来自知乎,侵删):
假设我们机器的字长是4,而数据是从索引为1的地方开始存放,我们的CPU将要访问两次,并分别丢弃地址0以及地址5-7中的内容。如果内存是对齐的,则只需要一次访问即可。这就是为什么要内存对齐,最直接的一个理由。
Go如何实现内存对齐
大小保证 Size guarantee
Go官网提到的基本数据类型的大小如下:
type size in bytes
byte, uint8, int8,bool 1
uint16, int16 2
uint32, int32, float32 4
uint64, int64, float64, complex64 8
complex128 16
我找到了一张更详细的表:
这张表是基于Go 1.14编译器的,我们可以发现结构体的size是可能需要进行内容填充以进行内存对齐的。此外,最后一行的意思,用官方的话说就是:如果一个结构体或数组类型不包含尺寸大于0的字段(或元素),那么它的尺寸就是零。两个不同的零尺寸变量在内存中可能有相同的地址。这是由于Go可能会将所有大小为0的类型,指向同一块内存。
对齐保证 Align guarantee
首先,Go官方对这个的介绍如下:
- 对于任何类型的变量x:unsafe.Alignof(x)至少是1
- 对于结构类型的变量x:unsafe.Alignof(x)是x的每个字段f的所有值unsafe.Alignof(x.f)中最大的一个,但至少是1
- 对于数组类型的变量x:unsafe.Alignof(x)与数组的元素类型的变量的对齐系数(alignment)相同(不是切片)
对齐系数(alignment),表明该类型占用空间必须是对齐系数的倍数。这里的unsafe.Alignof()
方法就是用于求任意类型的对齐系数的。Go的unsafe包中提供了几个方法,可以方便我们进行内存对齐的分析:
Function | Introduction | Explanation |
---|---|---|
unsafe.Alignof() |
获取该类型的对齐系数 | - 对于任何类型的变量x:unsafe.Alignof(x)至少是1。 - 对于结构类型的变量x:unsafe.Alignof(x)是x的每个字段f的所有值unsafe.Alignof(x.f)中最大的一个,且至少是1。 - 对于数组类型的变量x:unsafe.Alignof(x)与数组元素的类型对齐方式相同。 - 最大不会超过8(我觉得应该是一个字长),因为超过一个字长,就不需要对齐了 |
unsafe.Sizeof() |
获取该类型的占用的内存 | 没啥好说的 |
unsafe.Offsetof() |
获取结构体中字段的偏移值 | 即该字段起点与结构体起点之间的字节数,offset基本都是这个意思 |
零大小字段对齐
按理说,零大小的字段(比如struct{}
),对齐系数应该就是0,但是如果该字段在结构体的末尾,则可能需要额外花费一个对齐系数的内存空间。因为如果有指针指向了这个零大小字段,那么其指向的地址就是在结构体之外。而如果一直不释放这个指针,就会导致内存泄漏。
不过如果该字段在中间,或者其前一个字段包含了padding,则不会有这个问题。
内存对齐与效率问题
这个问题的核心很直观,我们希望尽可能地减少padding的长度。想做到这一点,就需要知道判断这个结构体的align的大小,以及该如何对字段进行排序。我们依然是通过例子来建立起认知。简单地对比几个结构体,就大概知道该如何优化了。
type A struct {
a int16 // 2
b string // 16
c int8 // 1
} // 32
type B struct {
a int16 // 2
c int8 // 1
b string // 16
} // 24
此外,还需要注意零大小字段的问题。如果自己拿捏不准,多用用上文提到的unsafe
包中的函数测试一下即可
Links
这篇文章由The Journeymen的Five Hundred Miles提供精神支持