1.Go中恰到好处的内存管理--segment@煎鱼的清汤锅

引入问题

type Part1 struct {
    a bool
    b int32
    c int8
    d int64
    e byte
}

首先先算一下part1占用的内存是多少?

func main() {
    fmt.Printf("bool size: %d\n", unsafe.Sizeof(bool(true)))
    fmt.Printf("int32 size: %d\n", unsafe.Sizeof(int32(0)))
    fmt.Printf("int8 size: %d\n", unsafe.Sizeof(int8(0)))
    fmt.Printf("int64 size: %d\n", unsafe.Sizeof(int64(0)))
    fmt.Printf("byte size: %d\n", unsafe.Sizeof(byte(0)))
    fmt.Printf("string size: %d\n", unsafe.Sizeof("EDDYCJY"))
}

输出结果:

bool size: 1
int32 size: 4
int8 size: 1
int64 size: 8
byte size: 1
string size: 16

这么看part1结构体占用内存大小就是1+4+1+8+1=15,这么算确实也没毛病,但其实不然;

真实的情况是什么样,看下面:

type Part1 struct {
    a bool
    b int32
    c int8
    d int64
    e byte
}

func main() {
    part1 := Part1{}
    
    fmt.Printf("part1 size: %d, align: %d\n", unsafe.Sizeof(part1), unsafe.Alignof(part1))
}

输出结果:

part1 size: 32, align: 8

可以看到最终输出Part1其实是占用32字节,与计算得到的15不一致,为什么呢?
那其实这里就要提到内存对齐这个概念。

内存对齐

首先要了解一点,内存读取的方式
在这里插入图片描述
上图表示按字节一个一个读取的方式,如果是这种读取方式,那我们计算的part1的长度应该是对的;但实际上CPU读取内存是一块一块读取的,块的大小可以是2,4,8,16字节等大小。这样的块大小称之为内存访问粒度。如下:
在这里插入图片描述
在这里,假设内存访问粒度为4。所以CPU应该是以4个字节大小的内存访问力度去读取和访问内存的。

什么时候需要用到内存对齐

  • 你正在编写的代码在性能(CPU、Memory)方面有性能要求
  • 你正在处理向量方面的指令
  • 某硬件平台(如 ARM)体系不支持未对齐的内存访问

为什么要用内存对齐

  • 平台(移植):不是所有的平台都支持访问任意地址上的任意数据,如上面提到的ARM;
  • 性能:访问未对齐的内存,会导致CPU 进行二次访问内存,并需要花额外的时间来处理对齐及运算。而对齐的内存仅需要一次访问就可以完成读写操作;
    在这里插入图片描述

上图中虚线为内存的访问边界,如果从Index1开始读取,会导致访问边界是不对齐的,因此CPU会做一下额外处理:

  • CPU 首次读取未对齐地址的第一个内存块,读取 0-3 字节。并移除不需要的字节 0
  • CPU 再次读取未对齐地址的第二个内存块,读取 4-7 字节。并移除不需要的字节 5、6、7 字节
  • 合并 1-4 字节的数据
  • 合并后放入寄存器

可见,不做内存对齐,是比较麻烦CPU的事,自然耗时就增加了;

而如果是做了内存对齐,上图从Index0开始读取,只需要读取一次也不需要额外的运算,提高了效率,达到空间换时间的目的。

默认对齐系数

不同的编译器默认系数不同,可使用预编译命令*#program pack(n)*变更,n表示“对齐系数”,常用两个平台的默认系数:

  • 32位:4
  • 64位:8

另外要注意,不同硬件平台占用的大小和对齐值都可能是不一样的。

成员对齐

func main() {
    fmt.Printf("bool align: %d\n", unsafe.Alignof(bool(true)))
    fmt.Printf("int32 align: %d\n", unsafe.Alignof(int32(0)))
    fmt.Printf("int8 align: %d\n", unsafe.Alignof(int8(0)))
    fmt.Printf("int64 align: %d\n", unsafe.Alignof(int64(0)))
    fmt.Printf("byte align: %d\n", unsafe.Alignof(byte(0)))
    fmt.Printf("string align: %d\n", unsafe.Alignof("EDDYCJY"))
    fmt.Printf("map align: %d\n", unsafe.Alignof(map[string]string{}))
}

输出结果:

bool align: 1
int32 align: 4
int8 align: 1
int64 align: 8
byte align: 1
string align: 8
map align: 8

Go中可以调用unsafe.Aligof来返回相应类型的对齐系数,基本上都是2^n,最大不超过8,因为当前64位编译器默认对齐系数是8;

整体对齐

成员变量需要做字节对齐,那么最终得到的结构体肯定也需要做字节 对齐的;

对齐规则
  • 结构体的成员变量,第一个偏移量为0,以后每一个成员变量的对齐值必须是编译器的 默认对齐系数#pragma pack(n))或者当前成员变量类型的长度unsafe.Sizeof),取最小值作为当前类型的对齐值,偏移量为对齐值的整数倍
  • 结构体本身,对齐值必须为编译器默认对齐长度#pragma pack(n))或结构体的所有成员变量类型中的最大长度,取最大数的最小整数倍作为对齐值
  • 结合以上两点,可得知若编译器默认对齐长度(#pragma pack(n))超过结构体内成员变量的类型最大长度时,默认对齐长度是没有任何意义的.
对齐分析

结构体*part1*

type Part1 struct {
    a bool
    b int32
    c int8
    d int64
    e byte
}
func main() {
    part1 := Part1{}
    fmt.Printf("part1 size: %d, align: %d\n", unsafe.Sizeof(part1), unsafe.Alignof(part1))
}

输出结果:

part1 size: 32, align: 8
成员变量类型偏移量自身占用
abool01
字节对齐13
bint3244
cint881
字节对齐97
dint64168
ebyte(uint8)241
字节对齐257
P1成员对齐:
  • a类型为bool,第一个变量偏移量0,占用1个字节。
  • b类型为int32,本身占用4个字节,根据规则一,偏移量为4,所以2-4的位置被padding(填补),axxx|bbbb。
  • c类型为int8,本身占1个字节,此时偏移量为8,c就在第9的位置,axxx|bbbb|cxxx|xxxx。
  • d为int64,占用8个字节,根据规则一,此时应偏移16,axxx|bbbb|cxxx|xxxx|dddd|dddd。
  • e为byte类型。占用1个字节,此时偏移量为24,e在25的位置,axxx|bbbb|cxxx|xxxx|dddd|dddd|e。
P1整体对齐

最后,变量对齐后,根据规则二,结构体本身也需要字节对齐,对齐值为8,此时为偏移为25,并不是8的整数倍,所以确定偏移量为32,对结构体对齐,结果为axxx|bbbb|cxxx|xxxx|dddd|dddd|exxx|xxxx。

结构体*part2*

type Part2 struct {
    e byte
    c int8
    a bool
    b int32
    d int64
}
func main() {
    part2 := Part2{}
    fmt.Printf("part2 size: %d, align: %d\n", unsafe.Sizeof(part2), unsafe.Alignof(part2))
}

输出结果:

part2 size: 16, align: 8
成员变量类型偏移量自身占用
ebyte01
cint811
abool21
字节对齐31
bint3244
dint6488
P2成员对齐:
  • e类型为byte,第一个变量偏移量0,占用1个字节。
  • c类型为int8,本身占用1个字节,偏移量为1,满足规则一,如下:ec。
  • a类型为bool,本身占1个字节,此时偏移量为2,c就在第3的位置,eca。
  • b为int32,占用4个字节,根据规则一,此时应偏移4,4的位置被padding,如下:ecax|bbbb。
  • d为int64类型。占用8个字节,此时偏移量为8,满足规则,不许额外补齐,即:ecax|bbbb|dddd|dddd
P2整体对齐

变量对齐后,符合规则二,即结构体part2的 内存占用就是16。

总结

  • 通过分析,可以发现之前推算的15确实是错误的,内存读取并非一个字节一个字节取得,而是一块一块的,通过空间换时间的思想来完成内存的读写。
  • 调整结构体内变量的字段顺序可以减少结构体占用内存大小,是巧妙的规避了padding的存在,让变量存放更加"紧凑",这一点对于加深Go的内存布局和大对象的优化很有帮助!!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值