聊聊go语言中的内存填充

写在文章开头

我们都知道数据加载到CPU缓存中可以提升执行性能,所以为了保证每一个结构体中的成员能够完整的被单个CPU核心加载以避免缓存一致性问题而提出内存对齐,这篇文章笔者会从go语言的角度来讨论这个优化机制。

Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili

因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 "加群" 即可和笔者和笔者的朋友们进行深入交流。

详解go语言中的内存对齐

内存填充代码示例

我们不妨举个例子,我们现在声明一个Num结构体并通过Sizeof获取其大小:

go 复制代码
type Num struct {
 num1 int32
 num2 int32
}

func main() {
 // 打印字节大小
 fmt.Println("Num bytes:", unsafe.Sizeof(Num{}))
}

输出结果为8字节,很明显两个32位的整型变量相加就是8字节:

python 复制代码
Num bytes: 8

我们再来看点神奇的,我们将num1改为int16,再次进行打印,理论上2byte+4byte最终输出应该是6byte

go 复制代码
type Num struct {
 num1 int16
 num2 int32
}

func main() {
 // 打印字节大小
 fmt.Println("Num bytes:", unsafe.Sizeof(Num{}))
}

但输出结果确是8byte,这是就是因为底层填充了2byte的内存空间:

python 复制代码
Num bytes: 8

详解内存对齐

我们假设Num未进行内存填充的Num 结构体在内存分配为struct-2,在它的地址空间前方有一个struct-1,在多核CPU64位操作系统下,数据都以一个字长即64bit加载,这就很可能导致一个完整的变量存在与不同的CPU核心中。 我们假设一种情况CPU-1操作struct-1,因为字长的原因导致加载数据时把struct-2的数据加载到CPU-1缓存中。同时CPU-2 Cache处理struct-2的业务逻辑,因为MESI协议,导致CPU-1中任何一个改动都会使得CPU-2缓存中的数据变成脏数据,出现缓存一致性问题。

考虑到这个问题,go语言便在struct-1空间的结尾填充了18bit使得内存空间占满1个字长,保证每一个变量都能通过一个字长的单位读取到:

内存对齐的工作机制

在进行不同的内存填充的时候,不同类型变量都着不同的对齐系数,例如布尔和int32对应的内存系统为1和4,以下图为例,布尔的对齐系统为1就意味着它的内存空间首地址能被1整除,所以我们分配为0x00,同理因为0x00被布尔占用,所以int32的内存空间地址分配到0x04,基于对齐系数这一计算可以确保了两个变量完整的占用了一个字长,且加载时能够保证每个字长加载的变量都是完整的,从而保证内存原子性:

csharp 复制代码
 // 不同类型对应的对齐系数
 fmt.Println("类型:", unsafe.Sizeof(false), " 对齐系数:", unsafe.Alignof(false))
 fmt.Println("类型:", unsafe.Sizeof(int32(1)), " 对齐系数:", unsafe.Alignof(int32(1)))
 fmt.Println("类型:", unsafe.Sizeof("hello"), " 对齐系数:", unsafe.Alignof("hello"))

对应输出结果:

makefile 复制代码
类型: 1  对齐系数: 1
类型: 4  对齐系数: 4 
类型: 16  对齐系数: 8

结构体中的内存对齐

我们再来看看结构体中对于内存对齐的使用,我们给出下面这段代码示例:

go 复制代码
type Obj struct {
 b   bool
 str string
 num int16
}

func main() {
 //输出其字节数
 fmt.Println(unsafe.Sizeof(Obj{}))
}

这段代码输出结果为32字节,原因很简单,bool为1字节,填充首位。然后string为2字节即(16bit)对应对齐系数为8,所以占用0x080x24的内存空间,最后int16对齐系数为2,于是从0x26开始填充2字节,即占用0x260x28,最后补齐剩余的4个字节空间,由此得出32字节

这明显因为变量排序不当导致boolstring之间空出了很多的内存空间,所以我们不妨将后面两个变量的顺序调换一下:

go 复制代码
type Obj struct {
 b   bool
 num int16
 str string
}

func main() {
 fmt.Println(unsafe.Sizeof(Obj{}))
}

最终输出结果变为24字节,因为int16对齐系统为2,所以bool之后空1格就可以完成对齐,随后移动4格保证字符串类型对齐,由此算出总空间为24字节,确保boolint161个字长(64bit),然后字符串用2个字长完成加载:

空结构体内存对齐问题

基于上述的例子,我们在结构体末尾加上1个空结构体:

go 复制代码
type Obj struct {
 b   bool
 num int16
 str string
 i   struct{}
}

func main() {
 //输出其字节数
 fmt.Println(unsafe.Sizeof(Obj{}))
}

最终输出结果为32,我们都知道空值默认都用zero-base,为了保证空结构体的地址空间不被其他成员错误利用,go语言会针对这种情况对结构体尾部进行一个内存填充,确保地址空间大小为32字节(字长的整数倍)

小结

本文通过几个简单的示例结合图解介绍了go语言如何通过内存填充的方式解决缓存一致性问题,我们来简单小结一下内存填充的几个要点:

  1. 通过对齐系数决定变量首地址值。
  2. 空结构体结尾需要填充空间避免地址复用异常。
  3. 整个结构体填充完成后需要保证是字长(64bit)的整数倍。

我是 sharkchiliCSDN Java 领域博客专家开源项目---JavaGuide contributor ,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。 因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 "加群" 即可和笔者和笔者的朋友们进行深入交流。

参考

CPU 缓存一致性:xiaolincoding.com/os/1_hardwa...

本文使用 markdown.com.cn 排版

相关推荐
红尘散仙5 小时前
我把终端小说阅读器接上了 AI Agent:TRNovel 现在能用 skill 生成书源了
人工智能·后端·rust
卷毛的技术笔记6 小时前
告别硬编码!Spring AI Alibaba 实现 AI Agent 智能工具调用(Tool Calling)
java·人工智能·后端·python·spring·ai编程
会编程的土豆6 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
喵个咪6 小时前
GoWind Toolkit Go后端代码生成 完整全流程实战
后端·go·orm
basketball6167 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang
qq_2518364577 小时前
SpringBoot+Vue 共享电池柜管理系统 完整实现 前后端分离项目实战 完整代码
vue.js·spring boot·后端
zhangxingchao7 小时前
AI 大模型核心六:量化、Workflow 与 Agent、多轮 RAG
前端·人工智能·后端
IT_陈寒8 小时前
Vite打包时遇到的坑,原来问题出在这里
前端·人工智能·后端
ayqy贾杰10 小时前
基层管理的三板斧,在AI时代行不通了
前端·后端·团队管理
Apifox10 小时前
Apifox 5 月更新|Postman 导入优化、Runner 支持非 root 运行、请求代码自动带鉴权
前端·后端·安全