菜鸟笔记
提升您的技术认知

go语言中的内存对齐是如何优化程序效率的?-ag真人游戏

有如下两个结构体:

type s1 struct {
  
	num1 int32   // 4字节
	num2 int32	  // 4字节
}
type s2 struct {
  
	num1 int16   // 2字节
	num2 int32   // 4字节
}

这两个结构体的大小是多少呢,使用sizeof来计算可以得到,s1和s2的大小都是8个字节,但是我们s2结构体中正常的计算两个成员大小得到的结果应该是6字节才对,这是为什么呢?

这是因为cpu访问内存是按照字长来进行的,所以如果每个变量如果跨字长的存放,也就是一个邻接一个来存放的话,这样当某一个变量存放的内存块恰好处于两个字长上(如下图所示),为了读取该数据cpu就需要读取两次,这种方式就会影响内存的原子性和效率。

因此出现了内存对齐这样的存放方式,在分配内存的时候如果发现该变量跨字长存放了,就将其分配到下一个字长上。这样的操作可以提高内存操作效率,有路由内存原子性。

补充知识 内存有三种性质:

  1. 原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
  2. 可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  3. 有序性:即程序执行的顺序按照代码的先后顺序执行。

为了方便内存对齐,go语言中提供了对齐系数,可以使用如下函数来获取相应的对齐数:

unsafe.alignof()

对齐系数指的是变量的内存地址必须被对齐系数整除,比如说对齐系数为4,那么变量内存地址必须是4的倍数。对于基础类型的变量,他们的字节长度和对齐系数相同,

结构体对齐分为内部对齐结构体之间对齐

  1. 内部对齐:考虑成员大小和成员的对齐系数,指的是结构体内部成员的相对结构体地址的位置,也被称为偏移量,要求每个成员的偏移量是自身大小和其对齐系数较小值的倍数;
  2. 结构体之间对齐:其实是对结构体长度进行填充,需要考虑对齐系数和系统字长,结构体长度是最大成员长度系统字长较小的整数倍。

举例说明,我们拥有一个结构体代码如下:

type demo struct {
  
	a bool  // 大小为1,对齐系数为1
	b string  // 大小为16,对其系数为8
	c int16  // 大小为2,对齐系数为2
}

首先考虑结构体的内部对齐,结构体地址即是结构体中第一个元素所处的位置,在这个结构体中就是bool类型变量a的位置,因为string类型的自身大小和对齐系数中对齐系数较小,故而变量b的偏移量就是8的倍数,我们选择变量b前一个变量后最小的8的倍数的位置,如下图所示,同理变量c也是一样的。

这里有些同学可能会有些疑问,变量a和变量b之间还有7个字节没有使用,变量c可以放在这些位置吗?答案是不可以的,因为结构体成员的位置应该严格按照结构体中成员定义顺序排列。所以我们一般情乱下会直接将此时结构体中的空内存使用零值进行填充。

然后就需要考虑结构体自身的内存对齐了,如我们上文中进行内部填充之后,此时结构体长度为28,在64位系统中系统字长为8字节,也就是说此时结构体的长度应该是8的倍数,故而需要对其继续进行填充,将其补齐到32字节,如下图所示:

读者可以发现结构体中成员之间的定义顺序显然会影响该结构体的长度,所以我们可以通过尝试调整成员变量的顺序,来节约空间,我们将原结构体更改如下:

type demo struct {
  
	a bool  // 大小为1,对齐系数为1
	c int16  // 大小为2,对齐系数为2
	b string  // 大小为16,对其系数为8
}
}

此时结构体的长度就变成了24字节,具体内存分布如下:

除了以上的补齐和偏移量,我们还需要使用结构体的对齐系数来确定结构体中第一个成员的位置,结构体的对齐系数时结构体成员中最大的对齐系数,也就是说结构体中第一个成员地址必须是结构体对齐系数的倍数

空结构体大小为0,当它独立出现的时候,地址为zerobase。但是当空结构体作为结构体中某一个成员的时候,它的地址应该跟随前一个变量,我们假定有如下这样一个结构体:

type demo struct {
  
	a bool
	z struct{
  }
	c int16
	b string
}

此时我们需要分配给这个空结构体成员一个地址,这个地址应该跟随前一个变量a,故而我们直接分配为0x01即可。

但是当空结构体出现在结构体末尾时,为了防止该空结构体的地址发生了二次分配,我们需要对该结构体进行填充,如下图所示,我们将结构体长度扩充为32字节。

网站地图