亲测体验Go语言PGO

本文是对官方 Profile-guided optimization in Go 1.21[1] 的学习与实践.


对于PGO的思路,之前就有过类似的想法,有些许差异. 但本质都是通过对以往运行情况的"学习",优化以后程序的运行(有点以史为鉴鉴于往事,资于治道的感觉)

alt

过程很简单:

  1. 收集程序运行过程中的数据。

  2. 编译器根据收集到的数据来分析程序行为,进而做出针对性的性能优化


Profile-guided optimization (PGO). 通过分析Profile来提高程序运行时性能,也称为 profile-directed feedback(PDF)feedback-directed optimization(FDO), 是一项通用的优化技术,在其他语言/软件产品如Chrome中也有使用


亲测体验


项目初始化


alt

以下代码来自官方示例[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语句导入了一些需要使用的包,包括bytesiolognet/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

alt

模拟线上请求,获取profile文件


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

alt

得到profile文件后,可以停止两个程序


使用profile文件


当 Go 工具链在主包目录中找到名为 default.pgo 的配置文件时,它将自动启用 PGO。或者, go build 的 -pgo 标志采用用于 PGO 的配置文件的路径

alt
mv cpu.pprof default.pgo
go build -o markdown.withpgo

这样就有了两个二进制文件, markdown-nopgomarkdown.withpgo


可以通过 go version -m markdown.withpgo 检查构建过程中是否启用了 PGO

alt

性能对比


运行未经过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安装)

alt

尴尬...经过pgo优化,反而性能下降了~

-count=40 改为 -count=100,再次分别执行两个二进制&进行benchmark,之后对比结果

alt

在n=100情况下,有4%的提升..


相关原理(Under the hood)


详细过程参考官方原文的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被内联优化了

alt

alt
常量传播 是什么?有何作用?

常量传播(Constant Propagation)是编译器优化技术中的一种方法,它涉及在编译时替换那些值已知且不变的变量引用,用它们的实际值代替。这个过程可以减少程序运行时的计算量,提高程序执行的效率。

作用

  1. 提高性能:通过在编译时替换常量,减少运行时的计算,从而提升程序运行速度。
  2. 减少代码体积:有时候,常量传播可以帮助消除一些不必要的代码,从而减少最终程序的大小。
  3. 代码优化:作为编译器优化的一部分,它帮助生成更高效、更紧凑的代码。

实际的编译过程中,常量传播可能涉及更复杂的分析和替换,特别是在大型程序和复杂的代码结构中。这种优化有助于提高程序的执行效率,尤其是在涉及大量计算和逻辑判断的情况下。


去虚拟化


alt

在编程语言优化中,“去虚拟化”(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

本文由 mdnice 多平台发布

相关推荐

  1. list转树形,可用

    2023-12-09 01:32:02       39 阅读
  2. NGINX安装方法(可用)

    2023-12-09 01:32:02       36 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2023-12-09 01:32:02       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2023-12-09 01:32:02       100 阅读
  3. 在Django里面运行非项目文件

    2023-12-09 01:32:02       82 阅读
  4. Python语言-面向对象

    2023-12-09 01:32:02       91 阅读

热门阅读

  1. 点击登录按钮二次才跳转到首页

    2023-12-09 01:32:02       64 阅读
  2. 函数的参数

    2023-12-09 01:32:02       57 阅读
  3. AcGeLinearEnt2d::getPerpLine函数

    2023-12-09 01:32:02       57 阅读
  4. docker数据卷

    2023-12-09 01:32:02       60 阅读
  5. DVPP媒体数据处理视频编码问题案例

    2023-12-09 01:32:02       55 阅读
  6. React 笔记 父子组件传值

    2023-12-09 01:32:02       56 阅读
  7. 圣诞树网页效果代码详解

    2023-12-09 01:32:02       59 阅读
  8. scss:修改element组件样式(el-select)

    2023-12-09 01:32:02       57 阅读
  9. MySQL-DATE_FORMAT()函数

    2023-12-09 01:32:02       65 阅读