布景

Go 1.20版别于2023年2月份正式发布,在这个版别里引入了PGO功能优化机制。

PGO的英文全称是Profile Guided Optimization,基本原理分为以下2个过程:

  • 先对程序做profiling,收集程序运转时的数据,生成profiling文件。
  • 编译程序时启用PGO选项,编译器会依据.pgo文件里的内容对程序做功能优化。

咱们都知道在编译程序的时分,编译器会对程序做许多优化,包括咱们熟知的内联优化(inline optimization)、逃逸剖析(escape analysis)、常数传播(constant propagation)。这些优化是编译器能够直接经过剖析程序源代码来完成的。

但是有些优化是无法经过解析源代码来完成的。

比方一个函数里有许多if/else条件分支判别,咱们或许希望编译器主动帮咱们优化条件分支顺序,来加速条件分支的判别,提高程序功能。

但是,编译器或许是无法知道哪些条件分支进入的次数多,哪些条件分支进入的次数少,由于这个和程序的输入是有联系的。

这个时分,做编译器优化的人就想到了PGO: Profile Guided Optimization。

PGO的原理很简单,那就是先把程序跑起来,收集程序运转过程中的数据。然后编译器再依据收集到的程序运转时数据来剖析程序的行为,进而做针对性的功能优化。

比方程序能够收集到哪些条件分支进入的次数更多,就把该条件分支的判别放在前面,这样能够减少条件判别的耗时,提高程序功能。

那Go言语如何运用PGO来优化程序的功能呢?咱们接下来看看详细的比如。

示例

咱们完成一个web接口/render,该接口以markdown文件的二进制格局作为输入,将markdown格局转换为html格局回来。

咱们借助 gitlab.com/golang-commonmark/markdown 项目来完成该接口。

环境建立

$ go mod init example.com/markdown

新建一个 main.go文件,代码如下:

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 mod tidy
$ go build -o markdown.nopgo
$ ./markdown.nopgo
2023/02/25 22:30:51 Serving on port 8080...

程序主目录下新建input.md文件,内容能够自定义,契合markdown语法即可。

我演示的比如里用到了input.md 这个markdown文件。

经过curl指令发送markdown文件的二进制内容给/render接口。

$ curl --data-binary @input.md http://localhost:8080/render
<h1>The Go Programming Language</h1>
<p>Go is an open source programming language that makes it easy to build simple,
reliable, and efficient software.</p>
...

能够看到该接口回来了input.md文件内容对应的html格局。

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接口来获取程序运转时数据。

    $ curl -o cpu.pprof "http://localhost:8080/debug/pprof/profile?seconds=30"
    

​ 等待30秒,curl指令会完毕,在程序主目录下会生成cpu.pprof文件。

注意:要运用Go 1.20版别去编译和运转程序。

PGO优化程序

$ mv cpu.pprof default.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程序:

$ ./markdown.nopgo

敞开压力测验:

$ go test example.com/markdown/load -bench=. -count=20 -source ../input.md > nopgo.txt

敞开PGO优化的场景

启用敞开了PGO优化的server程序:

$ ./markdown.withpgo

敞开压力测验:

$ go test example.com/markdown/load -bench=. -count=20 -source ../input.md > withpgo.txt

综合对比

经过上面压力测验得到的nopgo.txtwithpgo.txt来做功能比较。

$ go install golang.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去优化程序,以提高体系处理功能。

更多关于PGO的运用说明和最佳实践能够参考profile-guided optimization user guide。

源代码地址:pgo optimization source code。

推荐阅读

  • Go 1.20来了,看看都有哪些改变

  • Go面试题系列,看看你会几题

  • Go常见错误和最佳实践系列

开源地址

文章和示例代码开源在GitHub: Go言语初级、中级和高档教程

公众号:coding进阶。

个人网站:Jincheng’s Blog。

知乎:无忌。

References

  • go.dev/blog/pgo-pr…