我从事项目的代码规模相当大,使用Golang编写,代码量约1000万行,开发者测试活动主要围绕IT(集成测试)进行,在本地编绎运行测试用例时,我的机器(i7-10700K)首次编绎需要耗时5分钟左右。我一直想找一些方法优化编绎速度,最近看到同事的帖子受到一些启发,通过某些配置或优化成功提升编绎速度59%。
[toc]
执行go test时发生了什么 以sort
包下的TestSearch
用例为例,可以通过下面的go test
命令执行测试用例:
1 2 3 4 5 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项目上试试看吧!