Go结构体内存对齐

先来看一个示例


package  main

import (

 "fmt"

 "unsafe"

)

type  struct1  struct {

 a  bool

 b  int64

 c  bool

}

type  struct2  struct {

 x  bool

 y  bool

 z  int64

}

func  main() {

 fmt.Printf("struct1   大小 %d 对齐系数 %d\n", unsafe.Sizeof(struct1{}), unsafe.Alignof(struct1{}))

 fmt.Printf("struct1.a 大小 %d 对齐系数 %d 偏移量 %d\n", unsafe.Sizeof(struct1{}.a), unsafe.Alignof(struct1{}.a), unsafe.Offsetof(struct1{}.a))

 fmt.Printf("struct1.b 大小 %d 对齐系数 %d 偏移量 %d\n", unsafe.Sizeof(struct1{}.b), unsafe.Alignof(struct1{}.b), unsafe.Offsetof(struct1{}.b))

 fmt.Printf("struct1.c 大小 %d 对齐系数 %d 偏移量 %d\n", unsafe.Sizeof(struct1{}.c), unsafe.Alignof(struct1{}.c), unsafe.Offsetof(struct1{}.c))

 fmt.Printf("\nstruct2   大小 %d 对齐系数 %d\n", unsafe.Sizeof(struct2{}), unsafe.Alignof(struct2{}))

 fmt.Printf("struct2.x 大小 %d 对齐系数 %d 偏移量 %d\n", unsafe.Sizeof(struct2{}.x), unsafe.Alignof(struct2{}.x), unsafe.Offsetof(struct2{}.x))

 fmt.Printf("struct2.y 大小 %d 对齐系数 %d 偏移量 %d\n", unsafe.Sizeof(struct2{}.y), unsafe.Alignof(struct2{}.y), unsafe.Offsetof(struct2{}.y))

 fmt.Printf("struct2.z 大小 %d 对齐系数 %d 偏移量 %d\n", unsafe.Sizeof(struct2{}.z), unsafe.Alignof(struct2{}.z), unsafe.Offsetof(struct2{}.z))

}

输出


struct1  大小  24  对齐系数  8

struct1.a  大小  1  对齐系数  1  偏移量  0

struct1.b  大小  8  对齐系数  8  偏移量  8

struct1.c  大小  1  对齐系数  1  偏移量  16

struct2  大小  16  对齐系数  8

struct2.x  大小  1  对齐系数  1  偏移量  0

struct2.y  大小  1  对齐系数  1  偏移量  1

struct2.z  大小  8  对齐系数  8  偏移量  8

内存布局示例图:

先说下为什么要内存对齐

许多计算机系统对基本数据类型的合法地址做出了一些限制,要求某种类型对象的地址必须是某个值 K(通常是 2、4 或 8)的倍数。这种对齐限制简化了形成处理器和内存系统之间接口的硬件设计。例如,假设一个处理器总是从内存中取 8 个字节,则地址必须为 8 的倍数。如果我们能保证将所有的 int64 类型数据的地址对齐成 8 的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个 8 字节内存块中。

无论数据是否对齐,X86-64 硬件都能正确工作。不过,Intel 还是建议要对齐数据以提高内存系统的性能。

上面这段来自于 csapp 3.9.3 章节 ,通俗点说就是 cpu 读取一般是读取固定位数,CPU访问非对齐的内存时为何需要多次读取。

go 的内存对齐

For the numeric types, the following sizes are guaranteed:


type                                 size in bytes

byte, uint8, int8                     1

uint16, int16                         2

uint32, int32, float32                4

uint64, int64, float64, complex64     8

complex128                           16

The following minimal alignment properties are guaranteed:

  • For a variable x of any type: unsafe.Alignof(x) is at least 1.

  • For a variable x of struct type: unsafe.Alignof(x) is the largest of all the values unsafe.Alignof(x.f) for each field f of x, but at least 1.

  • For a variable x of array type: unsafe.Alignof(x) is the same as the alignment of a variable of the array’s element type.

A struct or array type has size zero if it contains no fields (or elements, respectively) that have a size greater than zero. Two distinct zero-size variables may have the same address in memory.

对于数值类型,保证以下大小:


类型                                 大小(字节)

byte, uint8, int8                    1

uint16, int16                        2

uint32, int32, float32               4

uint64, int64, float64, complex64    8

complex128                          16

保证以下最小对齐属性:

  • 对于任何类型的变量 x:unsafe.Alignof(x) 至少为 1。

  • 对于结构体类型的变量 x:unsafe.Alignof(x) 是 x 中每个字段 f 的 unsafe.Alignof(x.f) 的最大值,但至少为 1。

  • 对于数组类型的变量 x:unsafe.Alignof(x) 与数组元素类型的变量的对齐方式相同。

如果一个结构体或数组类型不包含大小大于零的字段(或元素),则其大小为零。两个不同的零大小变量在内存中可能有相同的地址。

为什么上面的结构体布局是这样

对于 struct1 来说 b 的对齐系数是 8 所以不能仅跟着 a 而是需要位于第 8 字节处 然后 c 位于第17字节处 struct1 的整体对齐系数是 8 所以需要在 struct1 后面填充 7 字节 使其满足 struct1 的对齐系数。

这里不知道大家会不会有疑问,反正本人是有,为什么 struct1 的后面要填充 7 个字节 。答案很简单 看下面这个操作你就明白了


var  arr []struct1

如果 struct1 的后面不填充 7 个字节则无法保证 arr 里面结构体的内存对齐。 所以要要求结构体的大小是其偏移量的倍数。

疑问的解决来自于 stackoverflow

知道这个有什么作用

就像实例中看见的一样 同样是两个bool 一个 int64 字段的结构体,占用的内存大小却不一样,所以如果了解这个,写代码时你就可以节省内存。

也有一些开源工具是做这个的。

structslop 但是我记得我还见过别的 一时没找见。

一个注意点。

当 struct{} 作为结构体最后一个字段时,需要内存对齐。因为如果有指针指向该字段, 返回的地址将在结构体之外,如果此指针一直存活不释放对应的内存,就会有内存泄露的问题(该内存不因结构体释放而释放)。

参考

go.dev

Go 语言高性能编程

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!