0%

《Golang》内存对齐

看例子

首先我们看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
"unsafe"
)

type T1 struct {
a int8
b int64
c int16
}

type T2 struct {
a int8
c int16
b int64
}

func main() {
var t1 T1
var t2 T2
fmt.Println(unsafe.Sizeof(t1)) // 24
fmt.Println(unsafe.Sizeof(t2)) // 16
fmt.Printf("t1.a: %p, size: %d\n", &t1.a, unsafe.Sizeof(t1.a)) // t1.a: 0xc00011a000, size: 1
fmt.Printf("t1.b: %p, size: %d\n", &t1.b, unsafe.Sizeof(t1.b)) // t1.b: 0xc00011a008, size: 8
fmt.Printf("t1.c: %p, size: %d\n", &t1.c, unsafe.Sizeof(t1.c)) // t1.c: 0xc00011a010, size: 2
fmt.Printf("t2.a: %p, size: %d\n", &t2.a, unsafe.Sizeof(t2.a)) // t2.a: 0xc00010c010, size: 1
fmt.Printf("t2.c: %p, size: %d\n", &t2.c, unsafe.Sizeof(t2.c)) // t2.c: 0xc00010c012, size: 2
fmt.Printf("t2.b: %p, size: %d\n", &t2.b, unsafe.Sizeof(t2.b)) // t2.b: 0xc00010c018, size: 8
}

运行后发现,t1变量占用内存24个字节,t2只占用16字节。

T1和T2结构体成员是一毛一样的,只是成员的顺序稍有不同而已。这是为什么???

内存对齐

CPU如果是32位的,说明数据总线是32根线,可以与内存之间一次性收发4个字节的数据

CPU如果是64位的,说明数据总线是64根线,可以与内存之间一次性收发8个字节的数据

上面的例子中,a成员占用1个字节,c占用2个字节,b占用8个字节

如果按照a-b-c的顺序存放内存的话,根据对齐规则,布局将是这样的:

1 9 10 16 17 18 24
a 填充0 b b b b c c 填充0 填充0

总共占用24个字节

如果按照a-c-b的顺序存放内存的话,布局将是这样的:

1 2 3 9 16
a c c 填充0 b b b

总共占用16个字节

为什么a-b-c的顺序不能是如下所示布局呢?

1 2 9 10 11
a b b b c c

这样的话,只占用11个字节,不是更节约内存吗

当然不是

CPU访问内存具有多级缓存,每级缓存由一行行组成,CPU每次访问内存一般都是以64字节的大小为单位访问的(详情可查看操作系统原理类别下的文章《伪共享》),64位CPU也就是连续访问8次,8*8=64字节,访问到的64字节会放入CPU缓存,下次如果访问到的地址在缓存中有,那么CPU就不会查询内存了

a和b一起占用了1-9字节,假如临近b变量的一个变量被访问过,就会放入缓存行中,如果b刚好处于某缓存行(1-8是这个缓存行的最后8个字节)的最后,那么当cpu访问b时,虽然可以定位到b,但是取不出数据,因为b没有完整存在于缓存中,判定为命中不到缓存,那么b就需要去内存取

所以内存对其有利于缓存命中,从而提高效率

对齐规则

  • 结构体的成员变量,第一个成员变量的偏移量为 0。往后的每个成员变量的对齐值必须为编译器默认对齐长度(#pragma pack(n))或当前成员变量类型的长度(unsafe.Sizeof),取最小值作为当前类型的对齐值。其偏移量必须为对齐值的整数倍
  • 结构体本身,对齐值必须为编译器默认对齐长度(#pragma pack(n))或结构体的所有成员变量类型中的最大长度,取最大数的最小整数倍作为对齐值

对齐案例

1
2
3
4
5
6
7
type Part1 struct {
a bool
b int32
c int8
d int64
e byte
}
成员变量 类型 偏移量 自身占用
a bool 0 1
padding 1 3
b int32 4 4
c int8 8 1
padding 9 7
d int64 16 8
e byte 24 1
padding 25 7
总占用大小 - - 32
  • 第一个成员 a

    • 类型为 bool
    • 对齐值为 1 字节
    • 初始地址,偏移量为 0。占用了第 1 位
    • 第二个成员 b
  • 类型为 int32

    • 对齐值为 4 字节(编译器默认对齐长度一般是8字节,自身大小是4字节,取较小者4字节)
    • 根据规则 1,其偏移量必须为 4 的整数倍。确定偏移量为 4,因此 2-4 位为 Padding。而当前数值从第 5 位开始填充,到第 8 位。如下:axxx|bbbb
    • 第三个成员 c
  • 类型为 int8

    • 对齐值为 min(8, 1) = 1 字节
    • 根据规则1,其偏移量必须为 1 的整数倍。当前偏移量为 8。不需要额外对齐,填充 1 个字节到第 9 位。如下:axxx|bbbb|c…
    • 第四个成员 d
  • 类型为 int64

    • 对齐值为 min(8, 8) = 8 字节
    • 根据规则 1,其偏移量必须为 8 的整数倍。确定偏移量为 16,因此 9-16 位为 Padding。而当前数值从第 17 位开始写入,到第 24 位。如下:axxx|bbbb|cxxx|xxxx|dddd|dddd
    • 第五个成员 e
  • 类型为 byte

    • 对齐值为 min(8, 1) = 1 字节
    • 根据规则 1,其偏移量必须为 1 的整数倍。当前偏移量为 24。不需要额外对齐,填充 1 个字节到第 25 位。如下:axxx|bbbb|cxxx|xxxx|dddd|dddd|e…

下篇预告

逃逸分析




微信关注我,及时接收最新技术文章