如果你之前写过 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

重新排列结构体字段可以提高应用程序的内存使用效率和速度。

想象一下维护一个大量占用空间的结构体的复杂应用程序,这将是一个改变游戏规则的操作。