但可能你不知道,通过简单的重新排序struct字段,可以极大提高Go程序的速度和内存使用效率!
是不是难以置信?我们一起来看一下吧!
简单Demo
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
如你所见,它们在内存使用方面并不一样。
是什么原因导致两个完全相似的struct消耗的内存不同?
答案在于数据在计算机内存中的排列方式。
简而言之,数据结构对齐。
数据结构对齐
CPU以字(word)为单位读取数据,而不是字节(byte)。
64位系统中,一个word是8个字节,而32位系统中,一个word是4个字节。
简而言之,CPU以其字长的倍数读取内存地址。
想象一下,在64位系统中,为了获取变量passportNum,CPU需要两个周期来访问数据。
第一个周期将获取内存的0到7字节,下一个周期获取其余内存字节。
把它想象成一个笔记本,每页只能存储一个字大小的数据(在本例中为8字节)。如果passportNum分散在两个页,则需要两次读取才能检索到完整的数据。
非常低效。
因此需要数据结构对齐,让计算机将数据存储在等于数据大小倍数的地址上。
4字节数据只能从内存地址0或4开始
例如,2字节数据可以存储在内存0、2或4中,而4字节数据可以存储在内存0、4或8中。
通过简单的对齐数据,计算机确保可以在一个CPU周期内检索到变量passportNum。
数据结构填充
填充是实现数据对齐的关键。
计算机通过在数据结构之间填充额外的字节,从而对齐字段。
这就是额外内存的来源!
我们来回顾一下BadStruct和GoodStruct。
GoodStruct消耗更少的内存,仅仅因为与BadStruct相比,其struct字段顺序更合理。
由于填充,两个13字节的数据结构分别变成了16字节和24字节。
因此,可以仅仅通过对struct字段重新排序来节省额外的内存!
这种优化为什么重要?
问题来了,你为什么要关心这个?
两个方面,速度和内存使用。
我们做一个简单的基准测试来证明!
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()
}
}
对GoodStruct和BadStruct进行基准测试的方法是循环遍历数组,并将struct字段累加到变量中。
从结果中可以看出,遍历GoodStruct确实比BadStruct花费时间更少。
对struct字段重排序可以优化应用程序的内存使用和速度。
想象一下,维护一个具有大量结构体的大型应用程序,改变将会更为明显。
结语
好了,全文到此为止,我们以一个简单的行动呼吁来结束:一定要对struct结构字段进行重排序!