理解 Golang 结构体对齐
in Note with 0 comment
理解 Golang 结构体对齐
in Note with 0 comment

背景

很多高性能的开发都要抠细节,其中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

我们可以看到:

  1. Demo_1包含了两个int类型、在64位机器中都是占用8个字节,所以结构体总的占用内存就是8+8=16字节。
  2. Demo_2包含了int8int16两种,按照上面的分析,结构体所占用的内存应该为1+2=3,但是输出的结果却是4,多出来一个字节,原因就是内存对齐,所以一个结构体的长度 = 各变量的长度 + 内存对齐的大小

内存对齐

内存对齐的原因:cpu 访问内存是字长(一个字的长度)为单位进行读取,比如:32位的cpu字长是4字节,那么cpu访问内存的大小也是4个字节。

内存对齐的目的:减少cpu读取内存的频率,增大吞吐量。

同样是读取8个字节的数据,一次读取字节长为4个字节,则只需要读取2次即可,若不进行字节对齐,则有可能增加cpu访问内存的次数,例子如下:

2021-03-11T15:12:09.png

如图所示:

另外内存对齐之后,如果变量的大小不超过字长,每次原子访问对变量来说也是原子操作,有利于高并发场景下的性能提升。

获取内存对齐倍数

在前面的例子中Demo_2结构体理应占据 3 个字节,但是内存对齐的结果是占用了 4 个字节,在 Go 语音中的内存对齐规律,我们可以用unsafe.Alignof()函数来获取一个变量的内存对齐倍数。

unsafe.Alignof(Demo_1{}) // 8
unsafe.Alignof(Demo_2{}) // 2

开发中的技巧

合理的定义顺序

先来看看一个例子,int8int16, 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的对齐方式:

2021-03-11T15:21:57.png

Demo_2的对齐方式

由于 num2 的对齐倍数是由 num3 决定的,所以内存对齐需要额外的 2 个字节。

2021-03-11T15:22:45.png

所以,在对内存比较敏感的结构,我们可以通过合理定义变量的顺序来节省内存。

空结构体的对齐

在 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 中定义结构体尽量先定义内存占用比较小的变量,避免在内存对齐中浪费多余的内存,行程良好的编程习惯。

Responses