Go 内存分配:结构体中的优化技巧

TimLiuDream · · 889 次点击 · 开始浏览    置顶
这是一个创建于 的主题,其中的信息可能已经有所发展或是发生改变。

> 关注公众号【爱发白日梦的后端】分享技术干货、读书笔记、开源项目、实战经验、高效开发工具等,您的关注将是我的更新动力! 在使用Golang进行内存分配时,我们需要遵循一系列规则。在深入了解这些规则之前,我们需要先了解变量的对齐方式。 Golang的`unsafe`包中有一个函数`Alignof`,签名如下: ```go func Alignof(x ArbitraryType) uintptr ``` 对于任何类型为`v`的变量`x`,`AlignOf`函数会返回该变量的对齐方式。我们将对齐方式记为`m`。现在,Golang确保`m`是满足`变量x的内存地址 % m == 0`的最大可能数,也就是说,变量x的内存地址是m的倍数。 让我们来看看一些数据类型的对齐方式: - `byte`, `int8`, `uint8` -> 1 - `int16`, `uint16` -> 2 - `int32`, `uint32`, `float32`, `complex64` -> 4 - `int`, `int64`, `uint64`, `float64`, `complex128` -> 8 - `string`, `slice` -> 8 对于结构体中的字段,行为可能会有所不同,详细信息请参考包的文档。 为了更好地理解结构体内存分配的情况,我们将使用`unsafe`包中的另一个函数`Offsetof`。该函数返回字段相对于结构体起始位置的位置,换句话说,它返回字段起始位置与结构体起始位置之间的字节数。 ```go func Offsetof(x ArbitraryType) uintptr ``` 为了更好地理解结构体内存分配,让我们以一个示例结构体为例: ```go type Example struct { a int8 b string c int8 d int32 } ``` 现在,我们将找出类型为`Example`的变量所占用的总内存,并尝试优化分配。 ```go var v = Example{ a: 10, b: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus rhoncus.", c: 20, d: 100, } fmt.Println("字段a的偏移量:", unsafe.Offsetof(v.a)) // 输出:0 fmt.Println("字段b的偏移量:", unsafe.Offsetof(v.b)) // 输出:8 fmt.Println("字段c的偏移量:", unsafe.Offsetof(v.c)) // 输出:24 fmt.Println("字段d的偏移量:", unsafe.Offsetof(v.d)) // 输出:28 ``` 现在,问题出现了:“为什么结构体中字段b的偏移量是8?它应该是1,因为字段a的类型是int8,只占用1个字节。”回到字符串数据类型的对齐方式,它的值为8,这意味着地址需要被8整除,因此在其中插入了7个字节的“填充”,以确保这种行为。 为什么字段c的偏移量是24?字段b中的字符串看起来比16个字节要长得多,如果字符串的偏移量是8,那么字段c的偏移量应该更大一些。 上述问题的答案是,在Go中,字符串并不是在结构体内的同一位置分配内存的。有一个单独的数据结构来保存字符串描述符,并且该字符串描述符以原地方式存储在结构体中,用于类型为string的字段,该描述符的大小为16个字节。 现在,让我们来看看`unsafe`包中的另一个函数`Sizeof`。正如其名称所示,该函数估计并返回类型为x的变量所占用的字节数。 注意:它是根据结构体中可能存在的不同大小的字段来估计大小的。 ```go func Sizeof(x ArbitraryType) uintptr ``` 现在,让我们来看看我们的结构体Example的大小。 ```go fmt.Println("Example的大小:", unsafe.Sizeof(v)) // 输出:32 ``` 我们如何优化这个结构体以最小化填充呢? 为了优化这个结构体的内存,我们将查看不同数据类型的对齐方式,并尝试减少填充。让我们尝试将两个int8类型的字段放在一起。 ```go type y struct { a int8 c int8 b string d int32 } var v = y{} fmt.Println("字段a的偏移量:", unsafe.Offsetof(v.a)) // 输出:0 fmt.Println("字段b的偏移量:", unsafe.Offsetof(v.b)) // 输出:8 fmt.Println("字段c的偏移量:", unsafe.Offsetof(v.c)) // 输出:1 fmt.Println("字段d的偏移量:", unsafe.Offsetof(v.d)) // 输出:24 fmt.Println("Example的大小:", unsafe.Sizeof(v)) // 输出:32 ``` 太棒了,我们去掉了一些填充,但是为什么大小仍然是32?大小应该是1(a)+ 1(c)+ 6(填充)+ 16(b)+ 4(d)= 28 现在,当结构体的最后一个字段与架构的对齐要求不完全一致时,会在最后一个字段之后添加填充,以确保结构体的整体大小是其字段中最大对齐要求的倍数。因为字符串数据类型的最大对齐方式为8,所以额外添加了填充,使大小成为8的倍数,即在末尾填充了4个字节,使大小为32字节。 我们能否进一步减少填充,使其更加优化? 让我们尝试通过移动字段位置来实现。 ```go type y struct { b string d int32 a int8 c int8 } var v = y{} fmt.Println("字段a的偏移量:", unsafe.Offsetof(v.a)) // 输出:20 fmt.Println("字段b的偏移量:", unsafe.Offsetof(v.b)) // 输出:0 fmt.Println("字段c的偏移量:", unsafe.Offsetof(v.c)) // 输出:21 fmt.Println("字段d的偏移量:", unsafe.Offsetof(v.d)) // 输出:16 fmt.Println("Example的大小:", unsafe.Sizeof(v)) // 输出:24 ``` 我们可以看到,通过重新排列字段的位置,使得对齐需要最小化填充,我们已经将结构体的大小从32减小到24,这是内存优化的巨大进步,达到了25%。 当前的内存占用是16(b)+ 4(d)+ 1(a)+ 1(b)+ 2(填充)。 遗憾的是,由于语言和架构的限制,我们无法进一步去除填充。

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

889 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传