Golang 开发技巧 - 简单的数据对齐可提高程序速度和内存使用率

Golang进阶 · 2023-06-28
Golang 开发技巧 - 简单的数据对齐可提高程序速度和内存使用率

Golang 中的结构或 struct 是用户定义的类型,允许将可能不同类型的项分组/组合为单一类型。
可以说是一个不支持继承但支持组合的轻量级类。
我们使用 Golang 编写代码的时候,你肯定使用过struct
但是,你可能不知道的是,通过简单地重新排序结构中的字段,可以极大地提高 Go 程序的速度和内存使用率!

示例演示

type EmployeeStruct struct {
    IsPublic bool
    Age      int64
    Status   bool
    Name     string
    Image    float32
}

我们来看一下Employee结构体的内存大小:

  • IsPublic(boolean) 1 字节
  • Age(int64) 8 字节
  • Status(boolean) 1 字节
  • Name(string) 16 字节
  • Image(float32) 4 字节

总计: 30 字节

通过unsafe.Sizeof检查一下:

package main

import (
    "fmt"
    "unsafe"
)

type EmployeeStruct struct {
    IsPublic bool
    Age      int64
    Status   bool
    Name     string
    Image    float32
}

func main() {
    var employee EmployeeStruct
    fmt.Println(unsafe.Sizeof(employee))
}

执行后输出: 48字节,为什么呢?

处理器类型

我们知道CPU 分为32位、34位,但是,它是如何运作的呢?

0_skP5bgz2F0MndpT_.png

想象一下我们有一个 64 位的 CPU,每个时钟周期传输 64 位数据的能力。

时钟周期是CPU处理一个信息需要多少时间,CPU 32位在1个周期内转换4字节数据,CPU 64位在1个周期内转换8字节数据(32位= 4字节,64位= 8字节)

上面我们定义的EmployeeStruct结构体,并且计算得到了每个字段在内存中占用的字节数。下面我们看CPU处理信息需要多少时间呢?
先来看下面的一张图:

1_-REJfyhK35-QbF0AP9ei8Q.webp

图里面6个周期,每个周期有八个盒子,表示是CPU处理能力,为 8 字节

  • 周期1
    第一个属性是IsPublic,数据类型为布尔型bool 的大小为 1 个字节。因此,当前周期 1 由属性IsPublic(bool ~ 1 byte)填充。
    下一个属性是Age,数据类型为int64,int64的大小是8字节,由于第1个周期的剩余内存大小只有7个字节,因此第1个周期无法填充Age属性,因此,会进入第2个周期,但是,“剩余的空间呢,怎么办?”。
  • 周期2
    第2个周期由Age属性填充,数据类型为int64,大小为8字节,刚好填充满。
  • 周期3
    第三个属性Status,数据类型为bool,大小为1字节,填充(bool~1 byte),剩余内存大小为7字节,然后,下一个属性是Name,类型为字符串,大小为 16 字节,与周期1的情况相同,因为没有足够的空间容纳下一个属性,所以将在下一个周期中进入。所以意味着有7个字节的内存被浪费了
  • 周期 4 和周期 5
    第四个是Name,数据类型为字符串,大小为16字节。从图上面看,它将在两个周期中填充,周期 4 中填充 8 个字节,周期 5 中填充 8 个字节。
  • 周期6
    最后一个属性是Image, 数据类型为float32,大小为4字节,浪费剩余的 4 个字节

最后通过计算统计:

总时钟周期 = 6 个时钟周期
结构体大小 = 48 字节
浪费的总内存 = 18 字节

所以我们在开发的时候,如果对于一个比较大的数据结构体来说,可能会使结构体的大小变得更大。

那么,如何解决呢?

实际上我们可以根据数据类型的大小来组成序列,最简单的方法是:

按元素内存大小的降序排列字段
type EmployeeStruct struct {
    Name     string
    Age      int64
    Image    float32
    IsPublic bool
    Status   bool
}

这个时候我们再来看一下时间周期:

1_ktpfeG0oqPDaFFNQxr9rSQ.webp

  • 周期 1 和周期 2
    第一个属性是Name,数据类型为字符串,大小为16字节。因此,它将在两个周期中填充,周期 1 中填充 8 个字节,周期 2 中填充 8 个字节。
  • 周期3
    第3个周期由Age属性填充,数据类型为int64,大小为8字节
  • 周期4
    下一个属性是Image,数据类型为float32,大小为4字节。因此,当前周期 4 由属性 Image ( float32 ~ 4 字节)填充,剩余大小为 4 字节。

下一个属性是IsPublic(bool ~ 1 字节)和 Status(bool ~ 1 字节)。因为最后两个属性的总大小只有2个字节,所以我们可以将其放在循环4中。这样一来,循环4就被Image ( float32 ~ 4字节)、IsPublic (bool ~ 1字节)、Status(bool~1 字节)填满了,仅浪费 2 字节内存。

最后通过计算统计:

总时钟周期 = 4 个时钟周期
结构体大小 = 32 字节
浪费的总内存 = 2 字节
package main

import (
    "fmt"
    "unsafe"
)

type EmployeeStruct struct {
    Name     string
    Age      int64
    Image    float32
    IsPublic bool
    Status   bool
}

func main() {
    var employee EmployeeStruct
    fmt.Println(unsafe.Sizeof(employee)) // output: 32
}

结束

所以我们在开发的时候,重新排序结构字段是可以提高应用程序的内存使用率和运行速度的。

Golang进阶 struct 结构体 内存对齐
Theme Jasmine by Kent Liao