什么是内存对齐?
编译器会将数据按照特定的规则,把数据安排到合适的存储地址上,并占用合适的地址长度
为什么要内存对齐
保证程序顺利高效的运行,可以让CPU快速从内存中存取到字段,避免资源浪费
内存对齐规则
1、起始的存储地址 必须是 内存对齐边界 的倍数。
2、整体占用字节数 必须是 内存对齐边界 的倍数。
Tip:先声明两个概念 ↓ ↓
内存对齐边界:结构体所有元素中,哪个元素占用的字节数大,那么这个元素占用的字节数就是内存对齐边界
对齐边界:结构体中每个元素自己占用的字节数
通过下边的示例来理解内存对齐的规则
首先我们定义一个结构体:
type S struct {
A uint8// byte:1
B int32// byte:4
C int16// byte:2
D int64// byte:8
E [2]string // byte总和:32 -->string类型数组,总共2个元素,每个元素包含2部分内容:
// 内容1:ptr,指向存放数据的地址,byte:8:内容2:len,标识字符串长度的整数值,byte:8
F struct{}// zero size field
}
步骤一:确定内存对齐边界
统计出结构体中所有的元素分别占用的字节数,占用最大的字节数就是内存对齐边界,在这个示例中的内存对齐边界就是 8
Tip:如果不知道怎样确定内存对齐边界,可以使用unsafe.Alignof()函数打印每个元素的对齐系数,打印中的最大值就是内存对齐边界:
func main() {
fmt.Println(unsafe.Alignof(S{}.A))// output: 1
fmt.Println(unsafe.Alignof(S{}.B))// output: 4
fmt.Println(unsafe.Alignof(S{}.C))// output: 2
fmt.Println(unsafe.Alignof(S{}.D))// output: 8
fmt.Println(unsafe.Alignof(S{}.E))// output: 8
fmt.Println(unsafe.Alignof(S{}.F))// output: 1
fmt.Println(unsafe.Sizeof(S{}))//可以直接打印出结构体所占用的字节数
}
步骤二:确定起始存储地址
内存对齐规则第一条:起始的存储地址 必须是 内存对齐边界 的倍数。也就是(起始地址addr)%(内存对齐边界)=0,在我们示例中起始地址就是addr%8=0,我们从0地址开始,为了方便看,我们先用图来展示地址存储分布图
文章图片
起始地址为0,0%8=0,符合条件,那我们就从0开始存储数据
其中元素A占用1个字节,并且0%1=0,符合条件,第0个地址分配给A;
元素B占用4个字节,从1个地址分配的话,1%4≠0,只有4%4=0,所以把第4-7个地址分配给B;
以此类推······ 具体分配如下
元素A:1个字节,占用第0个相对地址空间,0%1 =0(起始地址为0,内存边界为1)
元素B:4个字节,占用第4-7个相对地址空间,4%4 =0(起始地址为4,内存边界为4)
元素C:2个字节,占用第8-9个相对地址空间,8%2 =0(起始地址为8,内存边界为2)
元素D:8个字节,占用第16-23个相对地址空间,16%8=0(起始地址为16,内存边界为8)
元素E:8*2*2个字节,占用第24-31和32-39和40-47和48-55个相对地址空间,24%8=0...(起始地址为24,内存边界为8...)
元素F:zero size field,占用第56个相对地址空间,56%1=0 (起始地址为56,内存边界为1)
最终内存在第56个相对地址空间分配完毕,但是我们的地址空间是从0开始计算的,所以目前来看结构体总共占用57个字节!
步骤三:确定结构体占用字节数
我们在步骤二中排列出了结构体中元素的存放地址,整体元素占用57个字节,但是到这里还不算完事儿,因为我们还没有执行第二条的内存对齐规则–>整体占用字节数 必须是 内存对齐边界 的倍数。我们的内存对齐边界为8,而57不是8的倍数,所以我们需要扩张字节空间到8的倍数,延伸到64,也就是扩张到到图中的第63个相对地址空间。这个结构体占用的字节数为64字节!
内存空间优化
通过上边的示例与图表我们不难看出,其中还有好多个地址空间被浪费掉了,这些没被利用的地址空间,go语言会进行padding操作来对这些空间进行填充,使这些空间变成合法的内存空间。
我们再思考一下,如何才能减少地址空间的浪费呢?能不能通过重新排列元素的位置来合理的分配地址空间呢?
答案当然是肯定的,我们可以通过合理排列元素的定义顺序来减少地址空间的浪费。
我们先看结论,下边是重新排列后的结构体:
type S1 struct {
A uint8
F struct{}
C int16
B int32
D int64
E [2]string
}
再看一下重新分配地址空间的图标:
文章图片
起始地址为0,0%8=0,符合条件,我们就从0开始存储数据
其中元素A占用1个字节,并且0%1=0,符合条件,第0个地址分配给A;
元素F占用1个字节,1%1=0,符合条件,将第1个地址分配给B;
以此类推······ 具体分配如下
元素A:1个字节,占用第0个相对地址空间,0%1 =0(起始地址为0,内存边界为1)
元素F:zero size field,占用第1个相对地址空间,1%1 =0(起始地址为1,内存边界为1)
元素C:2个字节,占用第2-3个相对地址空间,2%2 =0(起始地址为2,内存边界为2)
元素B:4个字节,占用第4-7个相对地址空间,4%4=0(起始地址为4,内存边界为4)
元素D:8个字节,占用第8-15个相对地址空间,8%8=0(起始地址为8,内存边界为8)
元素E:8*2*2个字节,占用第16-23和24-31和32-39和40-47个相对地址空间,16%8=0... (起始地址为16,内存边界为8...)
最终内存在第47个相对地址空间分配完毕,但是我们的地址空间是从0开始计算的,所以目前来看结构体总共占用48个字节!并且48还是内存对齐边界值8的整数倍,所以结构体最终占用48个字节!
【GO基础|Golang内存对齐】我们可以很明显的看出来,在我们改变元素的定义顺序后,占用的字节空间从64字节减少到了48字节,内存空间得到了充分的优化!!!这也是一个结论所在,我们在结构体定义变量的时候,尽量将相同类型的变量定义在一起,将占用字节较少的变量类型放在一块。这也只是我个人的一个结论,其中内容有问题的话,欢迎大家进行指正!感谢您的阅读!
推荐阅读
- golang详解|【Golang详解】深入了解map
- Golang|深入理解Golang之context
- 深入理解golang map
- 个人成长|gopher成长之路(四)(GO开发工程师写QT)
- golang|redis 缓存穿透,缓存击穿,缓存雪崩
- go语言专栏|go项目部署(docker部署go项目&直接运行二进制文件部署(两种方式进行部署))
- golang|Go操作Mysql数据库居然如此丝滑
- #|深入解析Kubernetes admission webhooks
- golang|LeetCode26 删除有序数组中的重复项 Go语言