优化一个已有的 Go 程序
一个 Go 程序在生成汇编前的工作大概分为这几步:
语法解析。由于 Go 语言语法相当简单,所以 Go 编译器使用的是一个手写的 LALR (1) 解析器,这部分跟今天的 bug 无关,细节略过不提。 类型检查。Go 是强类型静态类型语言,在编译期会对赋值、函数调用等过程做类型检查,判断程序是否合法。另外,这个步骤会将一些 Go 自带的泛型函数变换成具体类型的函数调用,比方说 make 函数,在类型检查阶段会根据类型检查的结果变换成具体的 makeslice/makemap 等。这部分也跟今天的 bug 无关。 中间代码 (IR)生成。为方便做跨平台代码生成,也为方便做编译优化,现代编译器通常会将语法树变成一个中间代码表示形式,这个表示形式的抽象程度通常是介于语法树和平台汇编之间。Go 选择的是一种静态单赋值 (SSA)形式的中间代码。这部分较为重要,接下来一个小节会展开详述一下。 编译优化。在生成了 SSA IR 之后,编译器会基于这个 IR 跑很多趟(pass)代码分析和改写,每个 pass 会完成一个优化策略。另外值得一提的是,Go 中很多强度削减类的策略是使用一种 DSL 描述,然后代码生成出实际的 pass 代码来的,不过这块跟今天内容没什么关系,感兴趣的同学可以下来看看。在文章的后续内容中,我们就会定位到导致本文中这个 bug 的具体的 pass,并看到那个 pass 中有问题的逻辑。
禁用默认事务 对于写操作(创建、更新、删除),为了确保数据的完整性,GORM 会将它们封装在事务内运行。但这会降低性能,你可以在初始化时禁用这种方式
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{ SkipDefaultTransaction: true, }) 缓存预编译语句 执行任何 SQL 时都创建并缓存预编译语句,可以提高后续的调用速度
// 全局模式 db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{ PrepareStmt: true, })
// 会话模式 tx := db.Session(&Session{PrepareStmt: true}) tx.First(&user, 1) tx.Find(&users) tx.Model(&user).Update("Age", 18) 带 PreparedStmt 的 SQL 生成器 Prepared Statement 也可以和原生 SQL 一起使用,例如:
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{ PrepareStmt: true, })
db.Raw("select sum(age) from users where role = ?", "admin").Scan(&age) 您也可以使用 GORM 的 API DryRun 模式 编写 SQL 并执行 prepared statement ,查看 会话模式 获取详情
选择字段 默认情况下,GORM 在查询时会选择所有的字段,您可以使用 Select 来指定您想要的字段
db.Select("Name", "Age").Find(&Users{}) 或者定义一个较小的 API 结构体,使用 智能选择字段功能
type User struct { ID uint Name string Age int Gender string // 假设后面还有几百个字段... }
type APIUser struct { ID uint Name string }
// 查询时会自动选择 id、name 字段 db.Model(&User{}).Limit(10).Find(&APIUser{}) // SELECT id, name FROM users LIMIT 10 迭代、FindInBatches 用迭代或 in batches 查询并处理记录
Index Hints Index 用于提高数据检索和 SQL 查询性能。 Index Hints 向优化器提供了在查询处理过程中如何选择索引的信息。与 optimizer 相比,它可以更灵活地选择更有效的执行计划
import "gorm.io/hints"
db.Clauses(hints.UseIndex("idx_user_name")).Find(&User{}) // SELECT * FROM users USE INDEX (idx_user_name)
db.Clauses(hints.ForceIndex("idx_user_name", "idx_user_id").ForJoin()).Find(&User{}) // SELECT * FROM users FORCE INDEX FOR JOIN (idx_user_name,idx_user_id)"
db.Clauses( hints.ForceIndex("idx_user_name", "idx_user_id").ForOrderBy(), hints.IgnoreIndex("idx_user_name").ForGroupBy(), ).Find(&User{}) // SELECT * FROM users FORCE INDEX FOR ORDER BY (idx_user_name,idx_user_id) IGNORE INDEX FOR GROUP BY (idx_user_name)" 读写分离 通过读写分离提高数据吞吐量,查看 Database Resolver 获取详情
Profiling
那接下来我们给main.go程序做profiling,得到程序运行时的数据,然后通过PGO来做性能优化。
在main.go里,有import net/http/pprof 这个库,它会在原来已有的web接口/render的基础上,新增一个新的web接口/debug/pprof/profile,我们可以通过请求这个profiling接口来获取程序运行时的数据。
-
在程序主目录下,新增load子目录,在load子目录下新增
main.go的文件,load/main.go运行时会不断请求上面./markdown.nogpo启动的server的/render接口,来模拟程序实际运行时的情况。$ go run example.com/markdown/load
请求profiling接口来获取程序运行时数据。
ruby
复制代码
$ curl -o cpu.pprof "http://localhost:8080/debug/pprof/profile?seconds=30"
复制代码
等待30秒,curl命令会结束,在程序主目录下会生成cpu.pprof文件。
要使用Go 1.20版本去编译和运行程序。
PGO优化程序
bash
mvcpu.pprofdefault.pgo go build -pgo=auto -o markdown.withpgo
go build编译程序的时候,启用-pgo选项。
-pgo既可以支持指定的profiling文件,也可以支持auto模式。
如果是auto模式,会自动寻找程序主目录下名为default.pgo的profiling文件。
Go官方推荐大家使用auto模式,而且把default.pgo文件也存放在程序主目录下维护,这样方便项目所有开发者使用default.pgo来对程序做性能优化。
Go 1.20版本里,pgo选项的默认值是off,我们必须添加pgo=auto来开启PGO优化。
未来的Go版本里,官方计划将pgo选项的默认值设置为auto。
性能对比
在程序的子目录load下新增bench_test.go文件,bench_test.go里使用Go性能测试的Benchmark框架来给server做压力测试。
未开启PGO优化的场景
启用未开启PGO优化的server程序:
bash
$ ./markdown.nopgo
开启压力测试:
bash
$ go test example.com/markdown/load -bench=. -count=20 -source ../input.md > nopgo.txt
开启PGO优化的场景
启用开启了PGO优化的server程序:
bash 复制代码 $ ./markdown.withpgo
开启压力测试:
bash 复制代码 $ go test example.com/markdown/load -bench=. -count=20 -source ../input.md > withpgo.txt
综合对比
通过上面压力测试得到的nopgo.txt和withpgo.txt来做性能比较。
bash
���������������.���/�/����/���/����ℎ����@������goinstallgolang.org/x/perf/cmd/benchstat@latest benchstat nopgo.txt withpgo.txt goos: darwin goarch: amd64 pkg: example.com/markdown/load cpu: Intel(R) Core(TM) i5-5250U CPU @ 1.60GHz │ nopgo.txt │ withpgo.txt │ │ sec/op │ sec/op vs base │ Load-4 447.3µ ± 7% 401.3µ ± 1% -10.29% (p=0.000 n=20)
可以看到,使用PGO优化后,程序的性能提升了10.29%,这个提升效果非常可观。
在Go 1.20版本里,使用PGO之后,通常程序的性能可以提升2%-4%左右。
后续的版本里,编译器还会继续优化PGO机制,进一步提升程序的性能。
总结
Go 1.20版本引入了PGO来让编译器对程序做性能优化。PGO使用分2个步骤:
先得到一个profiling文件。
使用go build编译时开启PGO选项,通过profiling文件来指导编译器对程序做性能优化。
在生产环境里,我们可以收集近段时间的profiling数据,然后通过PGO去优化程序,以提升系统处理性能。