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

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

使用Golang进行内存分配时,我们需要遵循一系列规则。在深入了解这些规则之前,我们需要先了解变量的对齐方式。


Golang的unsafe包中有一个函数Alignof,签名如下:


func Alignof(x ArbitraryType) uintptr


对于任何类型为v的变量xAlignOf函数会返回该变量的对齐方式。我们将对齐方式记为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。该函数返回字段相对于结构体起始位置的位置,换句话说,它返回字段起始位置与结构体起始位置之间的字节数。


func Offsetof(x ArbitraryType) uintptr


为了更好地理解结构体内存分配,让我们以一个示例结构体为例:


type Example struct {
    a int8
    b string
    c int8
    d int32
}


,我们将找出类型为Example的变量所占用的总内存,并尝试优化分配。


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的变量所占用的字节数。


注意:它是根据结构体中可能存在的不同大小的字段来估计大小的。


func Sizeof(x ArbitraryType) uintptr


现在,让我们来看看我们的结构体Example的大小。


fmt.Println("Example的大小:", unsafe.Sizeof(v)) // 输出:32


我们如何优化这个结构体以最小化填充呢?


为了优化这个结构体的内存,我们将查看不同数据类型的对齐方式,并尝试减少填充。让我们尝试将两个int8类型的字段放在一起。


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字节。


我们能否进一步减少填充,使其更加优化?


让我们尝试通过移动字段位置来实现。


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(填充)。


遗憾的是,由于语言和架构的限制,我们无法进一步去除填充。

相关文章
|
7天前
|
存储 缓存 监控
|
11天前
|
缓存 算法 Java
Java中的内存管理:理解与优化
【10月更文挑战第6天】 在Java编程中,内存管理是一个至关重要的主题。本文将深入探讨Java内存模型及其垃圾回收机制,并分享一些优化内存使用的策略和最佳实践。通过掌握这些知识,您可以提高Java应用的性能和稳定性。
35 4
|
5天前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
27天前
|
Go
Go to Learn Go之结构体
Go to Learn Go之结构体
35 5
|
1月前
|
关系型数据库 Go 数据处理
高效数据迁移:使用Go语言优化ETL流程
在本文中,我们将探索Go语言在处理大规模数据迁移任务中的独特优势,以及如何通过Go语言的并发特性来优化数据提取、转换和加载(ETL)流程。不同于其他摘要,本文不仅展示了Go语言在ETL过程中的应用,还提供了实用的代码示例和性能对比分析。
|
10天前
|
数据处理 Python
如何优化Python读取大文件的内存占用与性能
如何优化Python读取大文件的内存占用与性能
39 0
|
1月前
|
Java Android开发 UED
安卓应用开发中的内存管理优化技巧
在安卓开发的广阔天地里,内存管理是一块让开发者既爱又恨的领域。它如同一位严苛的考官,时刻考验着开发者的智慧与耐心。然而,只要我们掌握了正确的优化技巧,就能够驯服这位考官,让我们的应用在性能和用户体验上更上一层楼。本文将带你走进内存管理的迷宫,用通俗易懂的语言解读那些看似复杂的优化策略,让你的开发之路更加顺畅。
50 2
|
1月前
|
存储 Shell Go
Go语言结构体和元组全面解析
Go语言结构体和元组全面解析
|
1月前
|
存储 Go
Go: struct 结构体类型和指针【学习笔记记录】
本文是Go语言中struct结构体类型和指针的学习笔记,包括结构体的定义、成员访问、使用匿名字段,以及指针变量的声明使用、指针数组定义使用和函数传参修改值的方法。
|
2月前
|
存储 缓存 JSON
一行代码,我优化掉了1G内存占用
这里一行代码,指的是:String.intern()的调用,为了调用这一行代码,也写了几十行额外的代码。