Intro

1 这是什么玩意?

测试环境的字长为8byte

首先,引入一个很简单的例子

  1. type A struct {
  2. num1 int32
  3. num2 float64
  4. }
  5. type B struct {
  6. num1 int64
  7. num2 float64
  8. }
  9. func main() {
  10. fmt.Println(unsafe.Sizeof(A{}))
  11. fmt.Println(unsafe.Sizeof(B{}))
  12. }

似乎答案很明显,A的大小应该是4 + 8 = 12,而B的大小应该是8 + 8 = 16。然而,输出结果是两个16。这时候就是发生了内存对齐,导致了struct占用的内存比想象中大。

2 为什么要内存对齐

首先,必须引入字长 word size的概念,简单地说就是CPU一次性处理的数据长度。字长为32位的CPU就叫做32位CPU。字长影响的内容较多,在这不做过多的讨论,只要了解上述的概念就足够往下学习了。

操作系统访问内存并非逐字节进行访问的,而是逐字长的,也就是说,每次访问内存,都是从字长的整数倍的地址开始的

让我们看看下面这种情况(图片来自知乎,侵删):
image.png
假设我们机器的字长是4,而数据是从索引为1的地方开始存放,我们的CPU将要访问两次,并分别丢弃地址0以及地址5-7中的内容。如果内存是对齐的,则只需要一次访问即可。这就是为什么要内存对齐,最直接的一个理由。

Go如何实现内存对齐

Go官网 - Size and alignment guarantees

大小保证 Size guarantee

Go官网提到的基本数据类型的大小如下:

  1. type size in bytes
  2. byte, uint8, int8,bool 1
  3. uint16, int16 2
  4. uint32, int32, float32 4
  5. uint64, int64, float64, complex64 8
  6. complex128 16

我找到了一张更详细的表:
image.png
这张表是基于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的大小,以及该如何对字段进行排序。我们依然是通过例子来建立起认知。简单地对比几个结构体,就大概知道该如何优化了。

  1. type A struct {
  2. a int16 // 2
  3. b string // 16
  4. c int8 // 1
  5. } // 32
  6. type B struct {
  7. a int16 // 2
  8. c int8 // 1
  9. b string // 16
  10. } // 24

此外,还需要注意零大小字段的问题。如果自己拿捏不准,多用用上文提到的unsafe包中的函数测试一下即可

Links

stackoverflow - 最详细的内存对齐的理由

这篇文章由The Journeymen的Five Hundred Miles提供精神支持