数据对齐是指数据在内存中的分配方式。规则的内存分配可以加速CPU访问内存速度。如果不了解数据对齐,会导致编写的程序消耗额外的内存,并且程序性能低下。
为了理解数据对齐是如何工作的,先看看如果没有对齐,会产生什么效果。现分配两个变量,一个类型为int32(32bits),另一个类型为int64(64bits)。
var i int32
var j int64
在没有数据对齐的情况下,在64位系统架构上,上述变量在内存中的分配方式如下图。变量j分配空间跨越两个字。如果CPU读取j中的内容,需要访问两次内存,而不是一次。
为了阻止上述情况发生,变量的内存地址应该是其自身大小的整数倍,这种约束就是数据对齐。在Go语言中,下面各类型对齐规则如下:
上述所有的数据类型都保证是对齐的:即它们的地址是其大小的倍数。例如,任何int32类型变量的地址都是4的整数倍。
回到开头的例子,下图显示了i和j在内存中分配的两种不同情况。情况1对应的情况是在变量i前面分配有1个32位的变量。情况2是通过填充方法,变量i在字长的开头分配,为了保持对齐(j的地址必须是64的倍数),变量j不能直接紧挨i, 只能在下一个64的倍数地址分配,图中的灰色格子即填充的32字节。
下面来看因为填充导致的问题,结构Foo内容如下。b1占1个字节,i占8个字节,b2占1个字节。
type Foo struct{
b1 byte
i int64
b2 byte
}
在64位系统架构中,结构Foo在内存中的结构如下图。由于i是int64类型,所以它的地址必须是8的整数倍。因此,它不可能挨着b1在0x01位置分配,最近适合它的位置在0x08。b2分配的地址需要是1的倍数,所以紧挨着i在0x10位置分配。
又由于结构体的大小必须是字长(8字节)的整数倍,所以它的大小不是17字节,而是24字节。在编译的时候,Go编译器会添加填充确保数据对齐。填充后结构如下。
type Foo struct{
b1 byte
_ [7]byte
i int64
b2 byte
_ [7]byte
}
每创建一个Foo对象,需要占用24字节内存空间,尽管只含有10字节数据(剩下的14字节为填充信息)。因为结构是一个原子单元,所以它永远不会被重新组织,即使在垃圾回收(GC)之后;它将总是占用24个字节的内存。注意,编译器不会重新排列字段,它只添加填充以保证数据对齐。
如何减少Foo占用内存空间呢?经验方法是重新排列字段顺序,按字段类型大小降序排列,本例中,先排int64,然后是两个byte类型。
type Foo struct{
i int64
b1 byte
b2 byte
}
调整字段顺序后Foo在内存的结构如下图,首先是字段i,占一整个字长(8字节),然后是b1和b2紧挨着,并且在同一个字长内。由于结构体总大小必须字长整数倍,所以调整后占用内存为16字节。通过这个小小顺序调整,减少了33%内存占用空间。
版本1相比版本2关键的影响是下面这个场景。有一个内存缓存,需要缓存所有的Foo对象,这种情况下节省的内存非常明显。即使没有缓存的场景,也会有其他影响。例如,如果频繁的创建Foo对象,并分配在了堆上,导致的结果是频繁的GC,影响整体应用性能。
此外,空间局部性对程序也有性能影响。例如,考虑下面的sum函数,接收一个Foo 切片,对里面的数据i进行求和。
func sum(foos []Foo) int64 {
var s int64
for i := 0; i < len(foos); i++ {
s += foos[i].i
}
return s
}
针对两个不同字段顺序的Foo对象,在两个缓存行大小(128字节)的空间内布局如下。图中每个灰色条代表8个字节的数据,更暗的条状是变量i所在的位置。
可以看到都是相同的2个缓存行大小,第一个版本只能容纳5个i变量,第二个版本能容纳8个i变量。所以迭代切片foos,第二个版需要的缓存行要少很多。
下面通过性能测试代码验证下程序性能是否符合我们预期:第二个版本要比第一个快。在我电脑上测得第二个版本比第一个版本快约25%。
性能测试代码如下:
const n = 1_000_000
var global int64
func BenchmarkSum1(b *testing.B) {
var local int64
s := make([]Foo1, n)
b.ResetTimer()
for i := 0; i < b.N; i++ {
local = sum1(s)
}
global = local
}
func BenchmarkSum2(b *testing.B) {
var local int64
s := make([]Foo2, n)
b.ResetTimer()
for i := 0; i < b.N; i++ {
local = sum2(s)
}
global = local
}
测试运行结果如下:
goos: darwin
goarch: amd64
pkg: alignment
cpu: Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz
BenchmarkSum1-4 872 1274668 ns/op
BenchmarkSum2-4 1183 952591 ns/op