看Go中的struct如何被优化,还有小插曲

我是一只可爱的土拨鼠,专注于分享 Go 职场、招聘和求职,解 Gopher 之忧!欢迎关注我。

欢迎大家加入Go招聘交流群,来这里找志同道合的小伙伴!跟土拨鼠们一起交流学习。

想必大家在coding的时候,很少对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 个字节。最后两个字段 GenderIsDeleted 被放在同一个块中,从而将未使用的字节数从 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, int81
uint16, int162
uint32, int32, float324
uint64, int64, float64, complex648
complex12816

保证以下最小对齐属性:

  1. 对于任何类型的变量xunsafe.Alignof(x)至少为 1。

  2. 对于struct 类型的变量xunsafe.Alignof(x)是所有字段字节对齐的最大值unsafe.Alignof(x.f),但至少为 1。

  3. 对于数组类型的变量xunsafe.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。希望今天的文章对大家有一些帮助,如有相关看法欢迎留言讨论。

参考资料

[1]

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


资料下载

点击下方卡片关注公众号,发送特定关键字获取对应精品资料!

  • 回复「电子书」,获取入门、进阶 Go 语言必看书籍。

  • 回复「视频」,获取价值 5000 大洋的视频资料,内含实战项目(不外传)!

  • 回复「路线」,获取最新版 Go 知识图谱及学习、成长路线图。

  • 回复「面试题」,获取四哥精编的 Go 语言面试题,含解析。

  • 回复「后台」,获取后台开发必看 10 本书籍。

对了,看完文章,记得点击下方的卡片。关注我哦~ 👇👇👇

如果您的朋友也在学习 Go 语言,相信这篇文章对 TA 有帮助,欢迎转发分享给 TA,非常感谢!331eb77b020932b283278fd273b3483d.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 Go 可以使用接口和反射机制实现策略模式的优化,具体步骤如下: 1. 定义一个接口,该接口包含一个方法,用于执行不同类型文件的转换。例如: ```go type ProtoConverter interface { Convert(file io.Reader) ([]byte, error) } ``` 2. 定义不同类型文件的转换策略类,实现上述接口。例如: ```go type JsonConverter struct {} func (j *JsonConverter) Convert(file io.Reader) ([]byte, error) { // 解析 JSON 文件并转换成 protobuf 消息 // 返回 protobuf 消息的二进制格式 } type XmlConverter struct {} func (x *XmlConverter) Convert(file io.Reader) ([]byte, error) { // 解析 XML 文件并转换成 protobuf 消息 // 返回 protobuf 消息的二进制格式 } ``` 3. 在通用函数使用反射机制,根据不同类型的文件选择对应的转换策略类。例如: ```go func ConvertFile(file io.Reader, converterType string) ([]byte, error) { var converter ProtoConverter switch converterType { case "json": converter = &JsonConverter{} case "xml": converter = &XmlConverter{} default: return nil, fmt.Errorf("unsupported converter type: %s", converterType) } // 使用反射机制调用 Convert 方法 value := reflect.ValueOf(converter) method := value.MethodByName("Convert") args := []reflect.Value{reflect.ValueOf(file)} result := method.Call(args) if result[1].Interface() != nil { return nil, result[1].Interface().(error) } return result[0].Bytes(), nil } ``` 4. 调用通用函数进行文件转换。例如: ```go file, err := os.Open("example.json") if err != nil { log.Fatal(err) } defer file.Close() data, err := ConvertFile(file, "json") if err != nil { log.Fatal(err) } // 处理转换后的数据 ``` 以上是在 Go 实现策略模式优化的一个示例,具体实现可以根据实际需求进行调整。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值