关于Go的内存对齐
今天看到群里有人提到内存对齐的东西,网上查阅了golang相关的内存对齐资料,特意整理了下,希望对大家有帮助。
看完这篇介绍。
我们将获得以下知识点:
1.什么是内存对齐?
2.为什么需要内存对齐?
3.如何进行内存对齐?
4.golang的内存对齐如何体现?
5.如何利用内存对齐来优化golang?
我们来先看一个结构体:
package main import ( "fmt" "unsafe" ) type Test1 struct { a bool b int32 c int8 d int64 e byte } 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("测试字符串值")) }
输出:
bool size: 1 int32 size: 4 int8 size: 1 int64 size: 8 byte size: 1 string size: 16
这么一算,Test1
这一个结构体的占用内存大小为 1+4+1+8+1 = 15 个字节。相信有部分博友是这么算的,貌似看上去也没什么毛病,
真实情况是怎么样的呢?我们实际调用看看,如下:
package main import ( "fmt" "unsafe" ) type Test1 struct { a bool b int32 c int8 d int64 e byte } func main() { test1 := Test1{} fmt.Printf("结构体 长度: %d, 对齐: %d\n", unsafe.Sizeof(test1), unsafe.Alignof(test1)) }
输出结果:
结构体 长度: 32, 对齐: 8
最终输出为占用 32 个字节。这与前面所预期的结果完全不一样。
从结果可以看到bool int8 int32 int64 byte的大小,但是相加的结果并不是Test1结构体的结果,从结果可以看出来,bool占用了一个字节后,下一个int32类型占用4字节时是有4个字节偏移的,所以一共占用了八个字节,向后推导发现有些类型占用了比类型本身字节更大的空间。
导致这个问题的原因就是内存对齐。这充分地说明了先前的计算方式是错误的。为什么呢?
在这里要提到 “内存对齐” 这一概念,才能够用正确的理解去计算,接下来我们详细的讲讲它是什么
什么是内存对齐
有的博友们可能会认为内存读取,就是一个简单的字节数组摆放
上图表示一个坑一个萝卜的内存读取方式。但实际上 CPU 并不会以一个一个字节去读取和写入内存。相反 CPU 读取内存是一块一块读取的,块的大小可以为 2、4、6、8、16 字节等大小。块大小我们称其为内存访问粒度。如下图:
在样例中,假设访问粒度为 4。 CPU 是以每 4 个字节大小的访问粒度去读取和写入内存的。为了进一步加深对内存对齐的正确理解。我们这样:
在想象中内存应该是一个一个独立的字节组成的。像这样:
事实上,别人是这样的:
内存是按照成员的声明顺序,依次分配内存,第一个成员偏移量是0,其余每个成员的偏移量为指定数的整数倍数。像这样进行内存的分配叫做内存对齐。
为什么要关心对齐
你正在编写的代码在性能(CPU、Memory)方面有一定的要求
你正在处理向量方面的指令
某些硬件平台(ARM)体系不支持未对齐的内存访问
另外作为一个工程师,你也很有必要学习这块知识点哦 :)
为什么要做对齐
平台(移植性)原因:
不是所有的硬件平台都能够访问任意地址上的任意数据。例如:特定的硬件平台只允许在特定地址获取特定类型的数据,否则会导致异常情况.并不是所有的硬件平台都能访问任意地址上的任意数据,会直接报错的!(解释:比如说有的cpu读取4个字节数据,要是没有内存对齐,从1开始那么内存就需要把0-7字节的全部取出来,再剔除掉1/5/6/7,增加了额外的操作,cpu不一定能这么搞,自然就报错了)。
性能原因:
若访问未对齐的内存,将会导致 CPU 进行两次内存访问,并且要花费额外的时钟周期来处理对齐及运算。而本身就对齐的内存仅需要一次访问就可以完成读取动作.访问未对齐的内存,需要访问两次;如果对齐的话就只需要一次了。(解释:比如取int64,按照8个位对齐好了,那获取的话直接就是获取8个字节就好了,边界好判断)。内存对齐原则
对齐值为系统默认对齐值和类型大小长度的最小值
结构体内部字段对齐值为默认对齐值和字段最大类型长度的最小值。
再说简单点就是二个原则:
1.具体类型,对齐值=min(编译器默认对齐值,类型大小Sizeof长度)
2.struct每个字段内部对齐,对齐值=min(默认对齐值,字段最大类型长度)
内存对齐的使用
由于内存对齐的存在,使得类型按照不同的顺序排列属性可能会得到不同的大小,所以在受限条件下设计类型时需要注意。结构体是平时写代码经常用到的。相同的成员,不同的排列顺序,会有什么区别?
我们举个例子:
package main import ( "fmt" "unsafe" ) type Test1 struct { a bool b int32 c int8 d int64 e byte } type Test2 struct { a bool e byte c int8 b int32 d int64 } func main() { t1 := Test1{} t2 := Test2{} fmt.Printf("test1 大小: %d\n", unsafe.Sizeof(t1)) fmt.Printf("test2 大小: %d\n", unsafe.Sizeof(t2)) }
输出:
test1 大小: 32 test2 大小: 16