Go内存对齐详解(go 内存对齐)
 南窗  分类:IT技术  人气:162  回帖:0  发布于1年前 收藏

前置知识

在《小许code:Go内存管理和分配策略》这篇分享中我们了解到Go是怎么对内存进行管理和分配的,那么用户的程序进程在linux系统中的内存布局是什么样的呢?我们先了解一下基础知识,然后再看Go的内存对齐。

进程内存空间布局

在Linux系统中,将虚拟内存划分为用户空间和内核空间,用户进程只能访问用户空间的虚拟地址,拿32位系统来说,进程内存布局在结构上是有规律的,如下图:

32位linux内核给每一个进程都分配4G大小的虚拟地址空间,有3G的用户态和1G的内核态,用户态主要存放我们应用程序定义的指令或者数据,局部变量存在于栈上,随着函数的运行,栈上开辟了内存,函数运行完成,栈上内存自动被系统回收。

CPU

CPU是计算机的核心,决定了计算机的数据处理能力和寻址能力。CPU一次(一个时钟内)能处理的数据的大小由寄存器的位数和数据总线的宽度(也即有多少根数据总线)决定,我们通常所说的多少位的CPU,除了可以理解为寄存器的位数,也可以理解数据总线的宽度,通常情况下它们是相等的。顺便也了解下以下总线的概念

数据总线:决定了CPU单次的数据处理能力,用于在CPU和内存之间传输数据位于主板之上,不在CPU中

地址总线:用于在内存上定位数据,指定在 RAM(Random Access Memory)之中储存的数据的地址

控制总线:传送控制信号和时序信号,将微处理器控制单元(Control Unit)的信号,传送到周边设备

CPU访问内存方式

CPU 访问内存时,并不是逐个字节访问,而是以字长(word size)为单位访问。比如 32 位的 CPU ,字长为 4 字节,那么 CPU 访问内存的单位也是 4 字节。这么设计的目的,是减少 CPU 访问内存的次数,提升 CPU 访问内存的吞吐量。比如同样读取 8 个字节的数据,一次读取 4 个字节那么只需要读取 2 次,同理64位CPU下,默认以8字节对齐。因为CPU对内存的读取操作是对齐的,采用不对齐的存储方式,会导致为了读取一个数据CPU要访问两次内存。

内存对齐

现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特 定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。

为什么要内存对齐?

(1) 平台原因:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

(2) 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问,提高了寻址效率。

(3) 空间原因:没有进行内存对齐的结构体或类会浪费一定的空间,当创建对象越多时,消耗的空间越多。

举个栗子看下内存对齐对寻址效率的提升:

图中变量 A占据 4 字节的空间,变量B占据8字节空间,内存对齐后,CPU 读取变量 B 的值只需要进行一次内存访问。如果不进行内存对齐,CPU 读取变量B的值需要进行 2 次内存访问。第一次访问得到B的第4-7位置4 个字节,第二次访问得到变量B的8-11位置后4个字节。

内存对齐规则

1.第一个成员在与结构体变量偏移量为0的地址处。

2.其他成员变量要对齐到对齐数(编译器默认的一个对齐数与该成员大小的较小值)的整数倍的地址处。

3.结构体总大小为最大对齐数(除了第一个成员每个成员变量都有一个对齐数)的整数倍。

4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

浅谈Go内存对齐

unsafe.Alignof

// 基础库 unsafe包
func Alignof(v ArbitraryType) uintptr

Alignof返回类型v的对齐方式(即类型v在内存中占用的字节数),若是结构体类型的字段的形式,它会返回字段f在该结构体中的对齐方式。

unsafe.Sizeof

// 基础库 unsafe包
func Sizeof(v ArbitraryType) uintptr

Sizeof返回类型v本身数据所占用的字节数。返回值是“顶层”的数据占有的字节数。例如,若v是一个切片,它会返回该切片描述符的大小,而非该切片底层引用的内存的大小

Go常用数据类型说明

在对Go内存对齐进行加深了解之前,我们先回归下基础,看看不同类型的对齐系数和占用字节数,array比较特殊,它跟类型和数组长度有关,可以看出所有类型的对齐系数不会大于8(64位系统)。

 var i1 = 1
 var i2 int32 = 1
 var b1 = false
 var map1 = make(map[string]int)
 var ch1 = make(chan string)
 var inter1 interface{}
 var str = ""
 a1 := [2]string{"1", "2"}
 s2 := []int{1}

 fmt.Println("int 类型:占用字节数", unsafe.Sizeof(i1), "对齐系数:", unsafe.Alignof(i1))  
 fmt.Println("int32 类型:占用字节数", unsafe.Sizeof(i2), "对齐系数:", unsafe.Alignof(i2))                
 fmt.Println("string 类型:占用字节数", unsafe.Sizeof(str), "对齐系数:", unsafe.Alignof(str))          
 fmt.Println("bool 类型:占用字节数", unsafe.Sizeof(b1), "对齐系数:", unsafe.Alignof(b1))              
 fmt.Println("map 类型:占用字节数", unsafe.Sizeof(map1), "对齐系数:", unsafe.Alignof(map1))          
 fmt.Println("chan 类型:占用字节数", unsafe.Sizeof(ch1), "对齐系数:", unsafe.Alignof(ch1))            
 fmt.Println("interface 类型:占用字节数", unsafe.Sizeof(inter1), "对齐系数:", unsafe.Alignof(inter1)) 
 //(注:数组元素类型的变量的对齐系数决定 所占用的字节数 = 数组长度 * 数据类型占字节数 )
 fmt.Println("array 类型:占用字节数", unsafe.Sizeof(a1), "对齐系数:", unsafe.Alignof(a1)) 
 fmt.Println("slice 类型:占用字节数", unsafe.Sizeof(s2), "对齐系数:", unsafe.Alignof(s2)) 

 输出结果:
 int 类型:占用字节数 8 对齐系数: 8
 int32 类型:占用字节数 4 对齐系数: 4
 string 类型:占用字节数 16 对齐系数: 8
 bool 类型:占用字节数 1 对齐系数: 1
 map 类型:占用字节数 8 对齐系数: 8
 chan 类型:占用字节数 8 对齐系数: 8
 interface 类型:占用字节数 16 对齐系数: 8
 array 类型:占用字节数 32 对齐系数: 8
 slice 类型:占用字节数 24 对齐系数: 8

我们用个简单的图来归纳更一目了然(哈哈,我比较喜欢图)

举个栗子

这里举例跟大家一样都是使用结构体进行举例说明,相对会更形象,但是其他数据类型也都是要内存对齐的,本例将用Coder1和Coder2两个结构体来看内存对齐的影响。

// 64位操作系统 对齐系数 8
type Coder1 struct {
 Age   int32  
 Name  string 
 GoPer bool   
}
fmt.Println("所占字节size", unsafe.Sizeof(Coder{ Age: 18,  Name: "xiaoxu", GoPer: true})) 
//输出结构:所占字节size:32
Coder1内存布局图

通过前面对不同类型的占用字节数和对齐系数的了解,根据对齐规则,我们Coder1结构体的三个字段逐个来看,结合Coder1的内存布局图进行分析。

Age:类型是 int32,对齐系数 4, 占用4字节,放在图中 0-3绿色部分位置

Name:类型是string,对齐系数8,占用16字节,所以4-7位会被编译器填充,所以Name字段在8-22黄色位置

GoPer:类型是bool,对齐系数1,占用1字节,所以在24位紫色位置,而25-31蓝色部分会被填充

满足结构体对齐规则,也是对齐数的整数倍。

再来看看变量顺序是经过排序的Coder2,看看内存对齐带来的影响

// 64位操作系统 对齐系数 8
type Coder1 struct {
 GoPer bool  
 Age   int32 
 Name  string 
}
fmt.Println("所占字节size", unsafe.Sizeof(Coder{GoPer: true, Age: 18,  Name: "xiaoxu"})) 
//输出结构:所占字节size:24
Coder2内存布局图

Coder2内存布局图分析:

GoPer:类型是bool,对齐系数1,占用1字节,所以在1位紫色位置

Age:类型是 int32,对齐系数 4, 占用4字节,放在图中 1-4绿色部分位置,因为Age占4字节,所以GoPer字段后不会被填充

Name:类型是string,对齐系数8,占用16字节,所以5-7位会被编译器填充,所以Name字段在8-23黄色位置

空结构体字段对齐规则

如果空结构体作为结构体的内置字段:当变量位于结构体的前面和中间时,不会占用内存;当该变量位于结构体的末尾位置时,需要进行内存对齐,内存占用大小和前一个变量的大小保持一致。

type Demo1 struct {
 a struct{}
 b int64
 c int64
}

type Demo2 struct {
 a int64
 b struct{}
 c int64
}

type Demo3 struct {
 a int64
 b int64
 c struct{}
}

func main() {
 fmt.Println("Demo1:占用字节数",unsafe.Sizeof(Demo1{})) // 16
 fmt.Println("Demo2:占用字节数",unsafe.Sizeof(Demo2{})) // 16
 fmt.Println("Demo3:占用字节数",unsafe.Sizeof(Demo3{})) // 24
}

编辑

添加图片注释,不超过 140 字(可选)

 标签:go,

讨论这个帖子(0)垃圾回帖将一律封号处理……