0%

GO编绎速度优化

我从事项目的代码规模相当大,使用Golang编写,代码量约1000万行,开发者测试活动主要围绕IT(集成测试)进行,在本地编绎运行测试用例时,我的机器(i7-10700K)首次编绎需要耗时5分钟左右。我一直想找一些方法优化编绎速度,最近看到同事的帖子受到一些启发,通过某些配置或优化成功提升编绎速度59%。

[toc]

执行go test时发生了什么

sort包下的TestSearch用例为例,可以通过下面的go test命令执行测试用例:

1
2
3
4
5
# -a:每次都重新编绎
# -x:显示 Go 工具链在编译和运行测试时执行的所有命令
# -count=1:不使用测试缓存(这个参数解决了我的项目门禁用例中一个“马后炮”的问题)
# > test.log 2>&1:将结果重定向到文件中方便查看
go test -a -x -count=1 -run ^TestSearch$ sort > test.log 2>&1

执行上述命令可以得到一段很长的回显(5000+行),从中能看到执行go test时都发生了什么。

编绎

可以从日志中看到go在此期间通过compile程序,将不同的go文件编绎成.a文件(go的归档文件),表示编绎后的包

1
2
3
4
5
6
7
8
9
10
11
12
test.log:
12: /usr/local/go/pkg/tool/linux_amd64/compile -o $WORK/b014/_pkg_.a -trimpath "$WORK/b014=>" -p internal/coverage/rtcov -std -complete -buildid dtuSBwdYW-OGnA1MSjA9/dtuSBwdYW-OGnA1MSjA9 -goversion go1.22.4 -c=4 -nolocalimports -importcfg $WORK/b014/importcfg -pack /usr/local/go/src/internal/coverage/rtcov/rtcov.go
13: /usr/local/go/pkg/tool/linux_amd64/compile -o $WORK/b009/_pkg_.a -trimpath "$WORK/b009=>" -p internal/unsafeheader -std -complete -buildid cwzjX3XBYecsYqxkVo2z/cwzjX3XBYecsYqxkVo2z -goversion go1.22.4 -c=4 -nolocalimports -importcfg $WORK/b009/importcfg -pack /usr/local/go/src/internal/unsafeheader/unsafeheader.go
14: /usr/local/go/pkg/tool/linux_amd64/compile -o $WORK/b015/_pkg_.a -trimpath "$WORK/b015=>" -p internal/godebugs -std -complete -buildid SaVkc0586JZwgoMO7juS/SaVkc0586JZwgoMO7juS -goversion go1.22.4 -c=4 -nolocalimports -importcfg $WORK/b015/importcfg -pack /usr/local/go/src/internal/godebugs/table.go
17: /usr/local/go/pkg/tool/linux_amd64/compile -o $WORK/b022/_pkg_.a -trimpath "$WORK/b022=>" -p internal/itoa -std -complete -buildid o5aD7UZtPDa3umw05j6L/o5aD7UZtPDa3umw05j6L -goversion go1.22.4 -c=4 -nolocalimports -importcfg $WORK/b022/importcfg -pack /usr/local/go/src/internal/itoa/itoa.go
99: /usr/local/go/pkg/tool/linux_amd64/compile -o $WORK/b039/_pkg_.a -trimpath "$WORK/b039=>" -p math/bits -std -complete -buildid 9JLPifiVRAh3D2u7R8oC/9JLPifiVRAh3D2u7R8oC -goversion go1.22.4 -c=4 -nolocalimports -importcfg $WORK/b039/importcfg -pack /usr/local/go/src/math/bits/bits.go /usr/local/go/src/math/bits/bits_errors.go /usr/local/go/src/math/bits/bits_tables.go
122: /usr/local/go/pkg/tool/linux_amd64/compile -o $WORK/b049/_pkg_.a -trimpath "$WORK/b049=>" -p encoding -std -complete -buildid EyBMx5wVroKLyV0AEQnH/EyBMx5wVroKLyV0AEQnH -goversion go1.22.4 -c=4 -nolocalimports -importcfg $WORK/b049/importcfg -pack /usr/local/go/src/encoding/encoding.go
131: /usr/local/go/pkg/tool/linux_amd64/compile -o $WORK/b069/_pkg_.a -trimpath "$WORK/b069=>" -p crypto/internal/alias -std -complete -buildid 0sYCSbSfL3_SoJuO98E2/0sYCSbSfL3_SoJuO98E2 -goversion go1.22.4 -c=4 -nolocalimports -importcfg $WORK/b069/importcfg -pack /usr/local/go/src/crypto/internal/alias/alias.go
171: /usr/local/go/pkg/tool/linux_amd64/compile -o $WORK/b072/_pkg_.a -trimpath "$WORK/b072=>" -p crypto/internal/boring/sig -std -buildid EntjhMjhiJFriirZybhm/EntjhMjhiJFriirZybhm -goversion go1.22.4 -symabis $WORK/b072/symabis -c=4 -nolocalimports -importcfg $WORK/b072/importcfg -pack -asmhdr $WORK/b072/go_asm.h /usr/local/go/src/crypto/internal/boring/sig/sig.go
187: /usr/local/go/pkg/tool/linux_amd64/compile -o $WORK/b094/_pkg_.a -trimpath "$WORK/b094=>" -p internal/platform -std -complete -buildid XGYb49Zsr7mAz8ASw4_-/XGYb49Zsr7mAz8ASw4_- -goversion go1.22.4 -c=4 -nolocalimports -importcfg $WORK/b094/importcfg -pack /usr/local/go/src/internal/platform/supported.go /usr/local/go/src/internal/platform/zosarch.go
188: /usr/local/go/pkg/tool/linux_amd64/compile -o $WORK/b040/_pkg_.a -trimpath "$WORK/b040=>" -p slices -std -complete -buildid XQ8WGCsae0_Y5AmxKXF6/XQ8WGCsae0_Y5AmxKXF6 -goversion go1.22.4 -c=4 -nolocalimports -importcfg $WORK/b040/importcfg -pack /usr/local/go/src/slices/slices.go /usr/local/go/src/slices/sort.go /usr/local/go/src/slices/zsortanyfunc.go /usr/local/go/src/slices/zsortordered.go
...

导入配置文件

下面这些命令告诉编绎器,当前包依赖哪些其它包,以及这些包的编绎结果(.a文件)存放在哪里。如:
packagefile bytes=/tmp/go-build1436555578/b047/_pkg_.a表示当前包依赖bytes包,bytes包编绎后的目标文件位于/tmp/go-build1436555578/b047/_pkg_.a

1
2
3
4
5
# import config
packagefile bytes=/tmp/go-build1436555578/b047/_pkg_.a
packagefile context=/tmp/go-build1436555578/b061/_pkg_.a
packagefile errors=/tmp/go-build1436555578/b004/_pkg_.a
packagefile flag=/tmp/go-build1436555578/b048/_pkg_.a

go vet

go vet是一个静态代码分析程序,用于检测go代码可能的错误,例如Printf的占位符数量和参数数量不一致

1
2
3
4
5
6
7
8
9
10
11
12
[~/go/src/playground]$ cat main.go
package main

import "fmt"

func main() {
fmt.Printf("%d")
}
[~/go/src/playground]$ go vet .
# playground
# [playground]
./main.go:6:2: fmt.Printf format %d reads arg #1, but call has 0 args

从日志能看到,在go test阶段有go vet执行:

1
2
3
4
cd /usr/local/go/src/errors
TERM='dumb' /usr/local/go/pkg/tool/linux_amd64/vet -unsafeptr=false -unreachable=false $WORK/b004/vet.cfg
cd /usr/local/go/src/sort
TERM='dumb' /usr/local/go/pkg/tool/linux_amd64/vet -unsafeptr=false -unreachable=false $WORK/b038/vet.cfg

链接

link这行命令是 Go 工具链中的 链接器 (link) 在工作,用于将编译后的目标文件(.a 文件)链接成一个可执行的测试二进制文件,这个二进制文件随后会被执行以运行测试用例。
-s -w这些是优化标志:
-s:省略符号表(调试信息)。
-w:省略 DWARF 调试信息。

1
/usr/local/go/pkg/tool/linux_amd64/link -o $WORK/b001/sort.test -importcfg $WORK/b001/importcfg.link -s -w -X=runtime.godebugDefault=httplaxcontentlength=1,httpmuxgo121=1,panicnil=1,tls10server=1,tlsrsakex=1,tlsunsafeekm=1 -buildmode=exe -buildid=P78JIG-rBoEH3w-duj6Y/sB6BnVLtI1XJG-p955yE/ZBW6FGiySlAiRrR0H0BY/P78JIG-rBoEH3w-duj6Y -X testing.testBinary=1 -extld=gcc $WORK/b001/_pkg_.a

用例执行

可以从日志中看到sort.test二进制文件执行

1
2
3
4
cd /usr/local/go/src/sort
$WORK/b001/sort.test -test.paniconexit0 -test.timeout=10m0s -test.count=1 -test.run=^TestSearch$
rm -rf $WORK/b001/
ok sort 0.002s

观察代码编绎执行用时

使用time程序观察go test命令用时,如下:4.113(秒)表示用时

1
2
[~/go/src/playground]$ time go test -a -x -count=1 -run ^TestSearch$ sort > test.log 2>&1
go test -a -x -count=1 -run ^TestSearch$ sort > test.log 2>&1 17.10s user 3.18s system 492% cpu 4.113 total

优化项

禁用内联

原理

内联可以优化程序编绎后的运行速度,但会减慢程序编绎时的速度,禁用内联在大型项目中,能显著减少编绎用时。
禁用内联的方法非常简单,在编绎参数后添加gcflags=all=-l即可,内联在编绎阶段生效,添加上面的参数后可以发现compile命令多了一个参数-l

1
/usr/local/go/pkg/tool/linux_amd64/compile -o $WORK/b015/_pkg_.a -trimpath "$WORK/b015=>" -p internal/godebugs -std -complete -buildid 369dqT601aiIFJVxSpxI/369dqT601aiIFJVxSpxI -goversion go1.22.4 -c=4 -l -nolocalimports -importcfg $WORK/b015/importcfg -pack /usr/local/go/src/internal/godebugs/table.go

Goland和VS code中配置禁用内联

禁用go vet

我从事的项目仓库门禁,已部署了Golangci-lint,所以go test阶段不需要再执行go vet,禁用go vet能优化用例耗时。
方法:编绎参数后添加vet=off即可。

去掉DWARF调试信息和符号表

DWARF

DWARF 是一种标准化的调试数据格式,用于描述程序的调试信息。它通常嵌入到可执行文件或目标文件中,供调试器(如 GDB 或 Delve)使用。

DWARF 调试信息包含以下内容:

  • 变量信息:包括全局变量、局部变量的名称、类型和作用域。
  • 函数信息:函数的名称、参数、返回值类型以及代码的起始和结束地址。
  • 源代码映射:将二进制代码地址映射到源代码的行号和文件名。
  • 数据结构描述:复杂数据类型(如结构体、数组)的布局和字段信息。
  • 堆栈帧信息:用于帮助调试器解析函数调用栈。

符号表

符号表是编译器或链接器生成的一种数据结构,用于存储程序中符号(如变量、函数、类型等)的相关信息。符号表的主要作用是帮助链接器和调试器解析程序中的符号。它包含的信息包括:

  • 符号名称:如变量名、函数名。
  • 符号地址:符号在内存或二进制文件中的地址。
  • 符号类型:如全局变量、局部变量、函数、常量等。
  • 作用域信息:符号的可见性(如局部、全局或文件作用域)。
    在 Go 程序中,符号表可以帮助调试器识别函数和变量的名称,并将它们与内存地址关联起来。

在门禁中执行用例刊,这两样信息都不是必要的,禁用之后可以些微提升一些编绎时的耗时和编绎后二进制的体积。

删除无用的依赖

通过go test -x可以看到哪些包参与了编绎,再结合项目分析有哪些包是不需要编绎的。
在我的项目中,有相当多的代码由工具生成,在执行测试用例时可以不执行,因此直接删除它们能显著提升编绎速度。如果包下只有部分代码是必要的,那么可以对包做拆分,把需要的单独抽取成一个包。
除了优化编绎速度,也能减少编绎后二进制或镜像的体积。

优化验证

很喜欢小黑的一句话:let’s try and see what will happen
在你的Go项目上试试看吧!