看Go中的struct如何被优化,还有小插曲
struct中的字段顺序
以下面的结构为例,咱们看看下面的结构体:
package main
import (
"fmt"
"time"
"unsafe"
)
type People struct {
ID int64 // Sizeof: 8 byte Alignof: 8 Offsetof: 0
Gender int8 // Sizeof: 1 byte Alignof: 1 Offsetof: 8
NickName string // Sizeof: 16 byte Alignof: 8 Offsetof: 16
Description string // Sizeof: 16 byte Alignof: 8 Offsetof: 32
IsDeleted bool // Sizeof: 1 byte Alignof: 1 Offsetof: 48
Created time.Time // Sizeof: 24 byte Alignof: 8 Offsetof: 56
}
func main(){
p := People{}
fmt.Println(unsafe.Sizeof(p))
}
// output
// 80
从上面的输出可以看出打印结果为 80 字节,但是所有字段加起来是66 字节。那额外的 14 个字节是怎么来的呢?想必大部分同学也很清楚。64 位CPU处理器每次可以以 64 位(8 字节)块的形式传输数据。32 位 CPU的话则是32 位(4 字节)。
第一个字段ID
占用 8 个字节,Gender
字段占用了1 个字节并有 7 个未使用的字节。
第二个和第三个字段为字符串类型为16字节,接下来是IsDeleted
字段,它需要 1 个字节并有 7 个未使用的字节。
最好的情况是是按字段的大小从大到小对字段进行排序。对上述结构体进行排序,大小减少到 72 个字节。最后两个字段 Gender
和 IsDeleted
被放在同一个块中,从而将未使用的字节数从 14 (2x7) 减少到 6 (1 x 6),在此过程中节省了 8 个字节。
type People struct {
CreatedAt time.Time // 24 bytes
NickName string // 16 bytes
Description string // 16 bytes
ID int64 // 8 bytes
Gender int8 // 1 byte
IsDeleted bool // 1 byte
}
func main(){
p := People{}
fmt.Println(unsafe.Sizeof(p))
}
下面咱们看看Go 白皮书[1]中对字节大小保证的一些说明:
对于数字类型[2],有下面的大小保证:
类型 | 占用字节大小 |
---|---|
byte, uint8, int8 | 1 |
uint16, int16 | 2 |
uint32, int32, float32 | 4 |
uint64, int64, float64, complex64 | 8 |
complex128 | 16 |
保证以下最小对齐属性:
对于任何类型的变量 x
:unsafe.Alignof(x)
至少为 1。对于struct 类型的变量 x
:unsafe.Alignof(x)
是所有字段字节对齐的最大值unsafe.Alignof(x.f)
,但至少为 1。对于数组类型的变量 x
:unsafe.Alignof(x)
与数组元素类型的变量的对齐方式相同。
如果struct或数组类型不包含大小大于零的字段(或元素),则其大小为零。两个不同的零大小变量在内存中可能具有相同的地址。
可以看出占用小于8 字节的 Go 类型有:
bool:1 个字节 int8/uint8:1 个字节 int16/uint16:2 个字节 int32/uint32/rune:4 字节 float32:4 字节 byte:1个字节
那么你知道了这些小于8字节的类型,要手动检查他的大小然后对其进行排序嘛,NONONO,小土下面给大家推荐一个linter
fieldalignment[3]来检查并进行正确地排序。
fieldalignment 小工具
这里小土给大家介绍一个检测和对齐结构体字段的小工具fieldalignment
,顾名思义就是字段对齐的意思。下面让我们在项目中安装和运行一下fieldalignment
。
安装fieldalignment
$go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest
先别着急运行,咱们先来看下filedalignment的使用,fieldalignment可以找到那些可以重新排列以减少内存的结构,并提供 建议编辑最紧凑的顺序。
fieldalignment介绍
$ fieldalignment
fieldalignment: find structs that would use less memory if their fields were sorted
Usage: fieldalignment [-flag] [package]
This analyzer find structs that can be rearranged to use less memory, and provides
a suggested edit with the most compact order.
Note that there are two different diagnostics reported. One checks struct size,
and the other reports "pointer bytes" used. Pointer bytes is how many bytes of the
object that the garbage collector has to potentially scan for pointers, for example:
struct { uint32; string }
have 16 pointer bytes because the garbage collector has to scan up through the string's
inner pointer.
struct { string; *uint32 }
has 24 pointer bytes because it has to scan further through the *uint32.
struct { string; uint32 }
has 8 because it can stop immediately after the string pointer.
Be aware that the most compact order is not always the most efficient.
In rare cases it may cause two variables each updated by its own goroutine
to occupy the same CPU cache line, inducing a form of memory contention
known as "false sharing" that slows down both goroutines.
Flags:
-V print version and exit
-all
no effect (deprecated)
-c int
display offending line with this many lines of context (default -1)
-cpuprofile string
write CPU profile to this file
-debug string
debug flags, any subset of "fpstv"
-fix
apply all suggested fixes
-flags
print analyzer flags in JSON
-json
emit JSON output
-memprofile string
write memory profile to this file
-source
no effect (deprecated)
-tags string
no effect (deprecated)
-test
indicates whether test files should be analyzed, too (default true)
-trace string
write trace log to this file
-v no effect (deprecated)
看帮助的说明这里小土总结一下fieldalignment的介绍:
fieldalignment 会有两个不同的报告,一个是检查结构体的大小。另一个报告所使用的指针字节数(是指gc会对struct中的这些字节进行潜在的指针扫描)。
struct { uint32; string } :16个指针字节,gc会扫描字符串的内部指针。 struct { string; *uint32 } : 24个指针字节,gc会进一步扫描 *uint32。 struct { string; uint32 }:8个指针字节,因为扫描到string会立马停止。
可以看出最紧凑的顺序并不总是最有效的。在极少数情况下,它可能会导致两个变量分别被自己的goroutine更新占用同一个CPU缓存线,从而引起一种被称为 "假共享 "的内存争夺。这样会降低了两个goroutine的速度。
运行fieldalignment
小土在项目中使用了fieldalignment
命令,可以看出检测出不少的不符合排序规则的struct,而且fieldalignment
将未对齐的字段进行了重新排序,再次执行可以看到就没有相关的提示了。从下面的检测信息中大家也可以看出未对齐的struct
中有8-64字节的空间浪费。struct
较多的项目,算下来也是一笔不小的开销(8B*1024=8K,觉得这些内存占用微不足道的同学也可以忽略哈)。
$fieldalignment -fix ./...
... # 前面代码就省略了
struct with 2568 pointer bytes could be 2560
struct with 56 pointer bytes could be 48
struct with 16 pointer bytes could be 8
struct with 16 pointer bytes could be 8
struct of size 80 could be 72
struct with 200 pointer bytes could be 176
struct with 104 pointer bytes could be 72
struct with 80 pointer bytes could be 72
struct with 32 pointer bytes could be 24
struct with 40 pointer bytes could be 32
struct with 104 pointer bytes could be 40
struct with 72 pointer bytes could be 56
struct of size 256 could be 248
struct with 64 pointer bytes could be 48
fieldalignment的小bug
经过小土前面一顿操作执行,在准备commit的时候发现之前struct中的注释居然变没了,于是小土也给Go官方提了一个小issue,https://github.com/golang/go/issues/54333,都好几天了也都没给回复,sad😭,看来这问题有点微不足道。希望在大家使用fieldalignment
的时候注意这一点,小土是在fix之后进行了一些注释恢复。
小结
简单总结一下,小土开始对struct中的字段字节对齐做了一些分析并推荐了一个对struct中的字段顺序错乱fix的工具fieldalignment。希望今天的文章对大家有一些帮助,如有相关看法欢迎留言讨论。
参考资料
Go 白皮书: https://go.dev/ref/spec#Size_and_alignment_guarantees
[2]数字类型: https://go.dev/ref/spec#Numeric_types
[3]fieldalignment: https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/fieldalignment
推荐阅读