在《小许code:Go内存管理和分配策略》这篇分享中我们了解到Go是怎么对内存进行管理和分配的,那么用户的程序进程在linux系统中的内存布局是什么样的呢?我们先了解一下基础知识,然后再看Go的内存对齐。
在Linux系统中,将虚拟内存划分为用户空间和内核空间,用户进程只能访问用户空间的虚拟地址,拿32位系统来说,进程内存布局在结构上是有规律的,如下图:
32位linux内核给每一个进程都分配4G大小的虚拟地址空间,有3G的用户态和1G的内核态,用户态主要存放我们应用程序定义的指令或者数据,局部变量存在于栈上,随着函数的运行,栈上开辟了内存,函数运行完成,栈上内存自动被系统回收。
CPU是计算机的核心,决定了计算机的数据处理能力和寻址能力。CPU一次(一个时钟内)能处理的数据的大小由寄存器的位数和数据总线的宽度(也即有多少根数据总线)决定,我们通常所说的多少位的CPU,除了可以理解为寄存器的位数,也可以理解数据总线的宽度,通常情况下它们是相等的。顺便也了解下以下总线的概念
数据总线:决定了CPU单次的数据处理能力,用于在CPU和内存之间传输数据位于主板之上,不在CPU中
地址总线:用于在内存上定位数据,指定在 RAM(Random Access Memory)之中储存的数据的地址
控制总线:传送控制信号和时序信号,将微处理器控制单元(Control Unit)的信号,传送到周边设备
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.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
// 基础库 unsafe包
func Alignof(v ArbitraryType) uintptr
Alignof返回类型v的对齐方式(即类型v在内存中占用的字节数),若是结构体类型的字段的形式,它会返回字段f在该结构体中的对齐方式。
// 基础库 unsafe包
func Sizeof(v ArbitraryType) uintptr
Sizeof返回类型v本身数据所占用的字节数。返回值是“顶层”的数据占有的字节数。例如,若v是一个切片,它会返回该切片描述符的大小,而非该切片底层引用的内存的大小
在对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的内存布局图进行分析。
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内存布局图分析:
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 字(可选)