3.6 内存对齐、内存布局是怎么回事?

字长(word size)

字长(word size),指的是 CPU 一次可以访问数据的最大长度:

  • 对于 32 位的 cpu 来说:word size 为 2^32,即 4 byte

  • 对于 64 位的 cpu 来说:word size 为 2^64,即 8 byte

两种内存布局

下边是一个结构体的两种内存布局方式

第一种:按顺序

代码

type Foo struct {
    A int8 // 1
    B int8 // 1
    C int8 // 1
}

type Bar struct {
    x int32 // 4
    y *Foo  // 8
    z bool  // 1
}

你觉得 Bar 对象会占用多少的内存? 可能很多人会下意识地回答 13

会回答 13 是因为你觉得该结构体的内存分配是下面这样按顺利分配的。

按照前面所介绍的 word size 为 8 来计算,使用这种分配方式,当你访问 bar.y 的时候,CPU 需要访问内存两次。

memory layout of Bar1

memory layout of Bar1

第二种:按字长

而如果使用下面这种方式,当你再次访问 bar.y 的时候,CPU 需要访问内存一次。

memory layout of Bar1

memory layout of Bar1

因此真正的答案是 24,这是一种典型的用空间换时间的方法 – 内存对齐

func main() {
    var bar Bar
    fmt.Println(unsafe.Sizeof(bar))  // 24
}

合理定义结构体

从以上可以发现,我们定义的结构体虽然不大,只占用 24个byte,但实际有用的只有 13 的byte,内存使用率只有 50% 左右,很有优化的必要性。

如果将第三个属性挪 y 的前面

memory layout of Bar2

memory layout of Bar2

那就可以省下来 1 个 byte 了

func main() {
    var bar Bar
    fmt.Println(unsafe.Sizeof(bar))  // 16
}

y 为什么占用 8 字节?

看完了上面的介绍,想必你一定有一个疑问: Foo 结构体实际占用 3个byte,为什么 Bar.y 却要占用 8个 byte 呢?

type Foo struct {
    A int8 // 1
    B int8 // 1
    C int8 // 1
}

type Bar struct {
    x int32 // 4
    y *Foo  // 8
    z bool  // 1
}

因为 Bar.y 表示的是一个指针,而指针的对齐系数是 8

func main() {
    var bar Bar
    fmt.Println(unsafe.Alignof(bar.y))  // 8
}

你大可将 y 改成普通对象

type Foo struct {
    A int8 // 1
    B int8 // 1
    C int8 // 1
}

type Bar struct {
    x int32 // 4
    y Foo  // 3
    z bool  // 1
}

这样一来,bar 对象就只占用一个字长

func main() {
    var bar Bar
    fmt.Println(unsafe.Sizeof(bar)) // 8
}