如果你之前写过 Golang 代码,你一定见过并实现过结构体类型 — struct。
但你可能不知道,仅仅通过重排你的结构体字段,你就可以极大地提高你的 Go 程序的速度和内存使用效率!
听起来太好了?让我们来看看吧!
简单示例type BadStruct struct {
age uint8
passportNum uint64
siblings uint16
}
type GoodStruct struct {
age uint8
siblings uint16
passportNum uint64
}
在上面的代码片段中,我们创建了两个完全相同字段的结构体。让我们编写一个简单的程序,分别输出它们的内存使用情况。
// Output
Bad struct is 24 bytes long
Good struct is 16 bytes long
你可以看到,它们的内存使用情况是不同的。
是什么导致两个完全相似的结构体占用不同数量的字节呢?
答案在于计算机内存中的数据排列方式。
简而言之,数据结构对齐。
数据结构对齐Photo by SHVETS production on Pexels
CPU 读取数据时是按字大小而不是按字节大小读取的。
在 64 位系统中,一个字是 8 个字节,而在 32 位系统中,一个字是 4 个字节。
简而言之,CPU 按其字大小读取地址。
passportNum
第一个周期将获取内存 0 到 7,而随后的周期将获取剩余的部分。
passportNum
这是低效的。
因此,数据结构对齐是必要的 — 计算机将数据存储在地址等于数据大小的倍数上。
4 个字节的数据只能从内存地址 0 或 4 开始存储
例如,2 个字节的数据可以存储在内存 0、2 或 4 中,而 4 个字节的数据可以存储在内存 0、4 或 8 中。
passportNum
数据结构填充
Photo by Angela Roma on Pexels
填充是实现数据对齐的关键。
计算机会在数据结构之间用额外的字节进行填充,以对齐它们。
这就是额外内存的来源!
BadStructGoodStruct
GoodStructBadStruct
由于填充,两个 13 个字节的数据结构分别变成了 16 个字节和 24 个字节。
因此,你可以通过简单地重新排列结构体字段来节省额外的内存!
为什么它很重要?现在来到了百万美元的问题,为什么你应该关心它?
主要有两个方面,速度和内存使用效率。
让我们做一个简单的基准测试来证明它!
func traverseGoodStruct() uint16 {
var arbitraryNum uint16
for _, goodStruct := range GoodStructArr {
arbitraryNum += goodStruct.siblings
}
return arbitraryNum
}
func traverseBadStruct() uint16 {
var arbitraryNum uint16
for _, badStruct := range BadStructArr {
arbitraryNum += badStruct.siblings
}
return arbitraryNum
}
func BenchmarkTraverseGoodStruct(b *testing.B) {
for n := 0; n < b.N; n++ {
traverseGoodStruct()
}
}
func BenchmarkTraverseBadStruct(b *testing.B) {
for n := 0; n < b.N; n++ {
traverseBadStruct()
}
}
GoodStructBadStruct
GoodStruct
重新排列结构体字段可以提高应用程序的内存使用效率和速度。
想象一下维护一个大量占用空间的结构体的复杂应用程序,这将是一个改变游戏规则的操作。