跳过正文

Golang之内存对齐

·110 字·1 分钟
Chuck Chan
作者
Chuck Chan
分享技术、思考与生活

Golang之内存对齐
#

现象
#

type foo struct {
	a bool   //1字节
	b int32  //4字节
	c int8   //1字节
	d int64  //8字节
	e byte   //1字节
}

func main() {
    f := foo{}
    fmt.Println("size of foo: ", unsafe.Sizeof(f))
}

这个输出结果是各个字段占用内存的和15字节吗?答案并不是,输出的结果是32字节,要大于所有字段占用内存大小的总和15字节。

内存对齐
#

可能很多人认为在结构体中各个字段的内存是连续的,cpu按各个字段的大小按需读取相应的值。

.png

但实际上,cpu读取内存是以块为单位的,块的大小可以为 2、4、6、8、16 字节等大小(也称为内存粒度),cpu每次都读去内存里的“若干块”数据。在内存分布上,内存也是“一块一块”分布的,数据不是按照其本身的大小排布,而是按照内存粒度的来排布,以方便cpu快速读取数据,这就是内存对齐。

为什么需要内存对齐?
#

1.平台原因:不是所有硬件平台都能访问任意地址上的任意数据。例如:特定的硬件平台只允许在特定地址获取特定类型的数据,否则会导致异常情况。

2.cpu访问内存时,数据结构应该尽可能在数据边界上对齐,如果内存未对齐,cpu会进行两次内存访问,并且要花费额外的时钟周期来处理对齐及运算。举个例子:

-1.png
  1. 假设某个数据没有按照内存对齐来分布,其数据在1-4字节中,且cpu每次能读取4字节的数据。
  2. cpu首次读去0-3字节数据,并去除掉0号字节。
  3. cpu第二次去读4-7字节数据,并去除掉5/6/7号字节。
  4. 合并1-4号字节数据,放入寄存器。

可以看到,如果没有做内存对齐,cpu需要两次才能读取到数据,并且会有一些额外的去处无效数据的操作。而如果做了内存对齐,只需要从0开始一次性读取4个字节即可,大大加快了读取的效率。但内存对齐会产生很多填充的数据块,所以内存对齐是一种空间换时间的策略。

内存对齐的规则
#

对齐系数:

  • 对齐系数指的是cpu从内存上读取数据的首地址都是内存系数的n倍,在不同硬件平台上对齐系数都可能是不一样的,一般来说32位的系统是4,64位的系统是8。

对齐规则:

  • 对于结构体的各个成员,第一个成员位于偏移为0的位置,结构体第一个成员的偏移量(offset)为0,以后每个成员相对于结构体首地址的offset都是该成员大小与有效对齐值中较小那个的整数倍,如有需要编译器会在成员之间加上填充字节。
  • 除了结构成员需要对齐,结构本身也需要对齐,结构的长度必须是编译器默认的对齐长度和成员中最长类型中最大的数据大小的倍数对齐。

内存对齐分析
#

已前面的foo结构体来分析下它的内存排布。

  1. 首先从0位置开始,a字段占用1个字节,1比对齐系数8小。(当前内存分布: a)
  2. b字段占用4个字节,4比对齐系数8小。(当前内存分布: axxx|bbbb)
  3. c字段占用1个字节,1比对齐系数8小。(当前内存分布: axxx|bbbb|c)
  4. d字段占用8个字节,8与对齐系数8相等。(当前内存分布: axxx|bbbb|cxxx|xxxx|dddd|dddd|)
  5. e字段占用1个字节,1比对齐系数8小。(当前内存分布: axxx|bbbb|cxxx|xxxx|dddd|dddd|e)
  6. 最后整个结构体还要整体对齐,成员中最大是8与对齐系数相等,所以需要填充。(axxx|bbbb|cxxx|xxxx|dddd|dddd|exxx|xxxx|)

最后结果是占用32个字节,与前面的输出符合。

内存对齐优化
#

同样是foo结构体,字段完全一样,如果将foo改成如下结构:

type foo struct {
	e byte   //1字节
	c int8   //1字节
	a bool   //1字节
	b int32  //4字节
	d int64  //8字节
}

func main() {
    f := foo{}
    fmt.Println("size of foo: ", unsafe.Sizeof(f))
}

根据内存对齐规则分析,这个foo的内存分布是ecax|bbbb|dddd|dddd,只占用16字节,比起前面的分析的32字节节省了不少空间,可以看出,如果将字段编排合理,让字段更加“紧凑”,就可以使得填充的数据变少,更加节省内存空间。