上期在尝试优化 Tidb 代码中 struct maligned 问题的时候发现有 struct 的 alignment 可以优化,具体是

pushDownJoin https://github.com/pingcap/tidb/blob/8184e69eb7f53a024b8e79fb7eb9e1c2a174c8e4/planner/cascades/transformation_rules.go#L837 PushSelDownJoin https://github.com/pingcap/tidb/blob/8184e69eb7f53a024b8e79fb7eb9e1c2a174c8e4/planner/cascades/transformation_rules.go#L939

type pushDownJoin struct {
}

type PushSelDownJoin struct {
	baseRule
	pushDownJoin
}

pushDownJoin 是空 struct,baseRule struct 里面有一个指针,在 64 位机器上 baseRule 的大小是 8 字节,但是 PushSelDownJoin 大小确是 16 字节。

如果把 pushDownJoin 和 baseRule 互换位置,PushSelDownJoin 大小就变成了 8 字节。

官方有个 issue,在 struct 末尾增加了 _ struct{} 来防止 unkeyed literals,但是最后把 empty struct 放到了最前面来减少内存分配

to prevent unkeyed literals. Trailing zero-sized field will take space.

https://go-review.googlesource.com/c/go/+/15660/

如果空 struct 在最后,而且不 pad 的话,pushDownJoin 的内存地址可能会下一个数据的地址一样,造成各种问题,我猜的。

简单的测试代码告诉我们内存能省就省

package main

import "testing"

type Empty struct{}

type Int64ThenEmpty struct {
	a uint64
	_ Empty
}

type EmptyThenInt64 struct {
	_ Empty
	a uint64
}

func BenchmarkEmpty1(b *testing.B) {
	for i := 0; i < b.N; i++ {
		c := make([]Int64ThenEmpty, 100000)
		c[0].a = 42
	}
}

func BenchmarkEmpty2(b *testing.B) {
	for i := 0; i < b.N; i++ {
		c := make([]EmptyThenInt64, 100000)
		c[0].a = 42
	}
}

// BenchmarkEmpty1-16          5545            213116 ns/op         1605664 B/op          1 allocs/op
// BenchmarkEmpty2-16         10000            117012 ns/op          802830 B/op          1 allocs/op