本文是对官方 Profile-guided optimization in Go 1.21[1] 的学习与实践.
对于PGO的思路,之前就有过类似的想法,有些许差异. 但本质都是通过对以往运行情况的"学习",优化以后程序的运行(有点以史为鉴和鉴于往事,资于治道的感觉)
过程很简单:
Profile-guided optimization (PGO). 通过分析Profile来提高程序运行时性能,也称为 profile-directed feedback(PDF)或feedback-directed optimization(FDO), 是一项通用的优化技术,在其他语言/软件产品如Chrome中也有使用
以下代码来自官方示例[2]
package main
import (
"bytes"
"io"
"log"
"net/http"
_ "net/http/pprof"
"gitlab.com/golang-commonmark/markdown"
)
func render(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed)
return
}
src, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("error reading body: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
md := markdown.New(
markdown.XHTMLOutput(true),
markdown.Typographer(true),
markdown.Linkify(true),
markdown.Tables(true),
)
var buf bytes.Buffer
if err := md.Render(&buf, src); err != nil {
log.Printf("error converting markdown: %v", err)
http.Error(w, "Malformed markdown", http.StatusBadRequest)
return
}
if _, err := io.Copy(w, &buf); err != nil {
log.Printf("error writing response: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
}
func main() {
http.HandleFunc("/render", render)
log.Printf("Serving on port 8080...")
log.Fatal(http.ListenAndServe(":8080", nil))
}
这段代码是一个使用Go语言编写的简单的Web服务器,提供了一个/render
的HTTP接口,用于将输入的Markdown文本转换为HTML并返回给客户端。
代码中的import
语句导入了一些需要使用的包,包括bytes
、io
、log
和net/http
等。其中net/http/pprof
包是用于性能分析。gitlab.com/golang-commonmark/markdown
是一个第三方Markdown解析库。
render
函数是一个HTTP请求处理函数,它接收POST请求并从请求的主体中读取Markdown文本。然后使用markdown
包将Markdown文本转换为HTML,并将结果写入响应的主体中,最后通过HTTP响应返回给客户端。
main
函数是程序的入口点。它注册了render
函数来处理/render
路径的请求,并启动一个HTTP服务器监听端口8080。一旦服务器启动,它将打印一条日志消息,并通过http.ListenAndServe
函数来接收和处理传入的HTTP请求。
整体上,这段代码实现了一个简单的Markdown转换服务,通过HTTP接口接收Markdown文本并返回转换后的HTML结果。你可以将这段代码编译并运行,然后通过发送POST请求到http://localhost:8080/render
来测试它。
go build -o markdown-nopgo
编译如上代码
./markdown-nopgo
执行
另起一个终端窗口,找一个markdown格式的文档,此处以 Go 项目中的 README.md为例, 获取该README.md: curl -o README.md -L "https://raw.githubusercontent.com/golang/go/c16c2c49e2fa98ae551fc6335215fadd62d33542/README.md"
请求接口: curl --data-binary @README.md http://localhost:8080/render
Go官方这篇博客的作者,写了一个简单的程序[3],来模拟线上的真实负载情况
可以通过执行 go run github.com/prattmic/markdown-pgo/load@latest
mock线上的真实请求
同时因为已经导入了 _ "net/http/pprof"
, 故而可以通过 curl -o cpu.pprof "http://localhost:8080/debug/pprof/profile?seconds=30"
获得profile
得到profile文件后,可以停止两个程序
当 Go 工具链在主包目录中找到名为 default.pgo 的配置文件时,它将自动启用 PGO。或者, go build 的 -pgo 标志采用用于 PGO 的配置文件的路径
mv cpu.pprof default.pgo
go build -o markdown.withpgo
这样就有了两个二进制文件, markdown-nopgo 和 markdown.withpgo
可以通过 go version -m markdown.withpgo
检查构建过程中是否启用了 PGO
运行未经过pgo优化的二进制程序 ./markdown-nopgo
, 然后执行go test github.com/prattmic/markdown-pgo/load -bench=. -count=40 -source $(pwd)/README.md > nopgo.txt
, 保存其benchmark结果
运行经过pgo优化的二进制程序./markdown.withpgo
,同样执行go test github.com/prattmic/markdown-pgo/load -bench=. -count=40 -source $(pwd)/README.md > withpgo.txt
最后通过 benchstat nopgo.txt withpgo.txt
对比结果
(如果没有安装benchstat,可通过go install golang.org/x/perf/cmd/benchstat@latest
安装)
尴尬...经过pgo优化,反而性能下降了~
将 -count=40
改为 -count=100
,再次分别执行两个二进制&进行benchmark,之后对比结果
在n=100情况下,有4%的提升..
详细过程参考官方原文的differential profiling(差异分析) --- 即 在程序运行时获取了优化前和优化后的cpu及heap(主要看总分配计数,即 go tool pprof -sample_index=alloc_objects
)相关的pprof文件,然后通过 go tool pprof -diff_base cpu.nopgo.pprof cpu.withpgo.pprof
进行对比
能够发现 垃圾回收和内存分配的成本得到了降低,原因是 总体分配的数量相比不启用PGO构建优化前更少
其中 mdurl.Parse
(该项目中的一个func)的内存分配次数从之前的近500万次减少为0
这是因为 在非 PGO 构建中, mdurl.Parse
被认为太大,不适合内联。然而,因为我们的 PGO profile文件表明对此函数的调用很热,所以编译器确实内联了它们。
比较cpu.nopgo.pprof和cpu.withpgo.pprof能看到mdurl.Parse
被内联优化了
常量传播(Constant Propagation)是编译器优化技术中的一种方法,它涉及在编译时替换那些值已知且不变的变量引用,用它们的实际值代替。这个过程可以减少程序运行时的计算量,提高程序执行的效率。
作用
实际的编译过程中,常量传播可能涉及更复杂的分析和替换,特别是在大型程序和复杂的代码结构中。这种优化有助于提高程序的执行效率,尤其是在涉及大量计算和逻辑判断的情况下。
在编程语言优化中,“去虚拟化”(Devirtualization)是一种优化技术,通常用于面向对象编程语言中。它的目的是提高程序的运行效率。为了理解去虚拟化,首先需要了解面向对象编程中的“虚拟函数”(或“虚拟方法”)的概念。 在面向对象的编程语言中,例如C++、Java或C#,虚拟函数是一种可以在派生类中被重写的成员函数。当通过基类的指针或引用调用这样的函数时,会发生动态绑定(或晚期绑定),即运行时根据对象的实际类型来决定调用哪个函数版本。这种机制支持多态,但也带来了性能成本,因为每次调用都需要通过虚拟表(v-table)来确定要执行的正确函数。 去虚拟化是一种编译器优化技术,旨在减少或消除这种运行时开销。如果编译器能够在编译时确定一个特定的虚拟函数调用实际上会调用哪个函数版本,那么它可以直接生成对该特定函数版本的调用,而无需通过虚拟表。这样可以减少间接调用,提高程序运行的效率。 去虚拟化的成功取决于编译器能够多大程度上分析和确定对象的实际类型。在某些情况下,例如当对象的类型在编译时是已知的,去虚拟化可以非常有效。然而,在其他情况下,特别是在涉及复杂继承和多态性的情况下,去虚拟化可能不那么容易实现。
更多参考:
PGO: 为你的Go程序提效5%[4]
Profile Guided Optimizations in Go[5]
Go1.20 那些事:PGO、编译速度、错误处理等新特性,你知道多少?[6]
Profile-guided optimization[7]
PGO 是啥,咋就让 Go 更快更猛了?[8]
探究 Go Profile-Guided Optimizations(PGO)[9]
一文读懂Go 1.20引入的PGO性能优化[10]
[1]
Profile-guided optimization in Go 1.21: https://go.dev/blog/pgo
[2]
官方示例: https://go.dev/blog/pgo
[3]
简单的程序: https://github.com/prattmic/markdown-pgo
[4]
PGO: 为你的Go程序提效5%: https://colobu.com/2023/09/13/pgo/
[5]
Profile Guided Optimizations in Go: https://landontclipp.github.io/blog/2023/08/25/profile-guided-optimizations-in-go/
[6]
Go1.20 那些事:PGO、编译速度、错误处理等新特性,你知道多少?: https://blog.csdn.net/EDDYCJY/article/details/128910616
[7]
Profile-guided optimization: https://go.dev/doc/pgo
[8]
PGO 是啥,咋就让 Go 更快更猛了?: https://juejin.cn/post/7168692708725227556
[9]
探究 Go Profile-Guided Optimizations(PGO): https://blog.csdn.net/RA681t58CJxsgCkJ31/article/details/127178724
[10]
一文读懂Go 1.20引入的PGO性能优化: https://zhuanlan.zhihu.com/p/609529412