大家好,我是煎鱼。

2023 年初,在 Go1.20,PGO 发布了预览版本。在本次 Go1.21 的新版本发布,修复了各种问题后,PGO 已经正式官宣生产可用。

今天这篇文章就是和大家一起跟着官方示例快速体验一下他的性能优化和使用。

温习一下 PGO

Profile-guided optimization (PGO),PGO 是计算机编程中的一种编译器优化技术,借助配置文件来引导编译,达到提高程序运行时性能的目的

翻译过来是使用配置文件引导的优化,能提供应用程序的性能。也被称为:

  • profile-directed feedback(PDF)
  • feedback-directed optimization(FDO)

该项优化是一个通用技术,不局限于某一门语言。像是我们常见很多应用都有所使用其来优化。如下几个案例:

  • Chrome 浏览器,在 64 位版本的 Chrome 中从 53 版开始启用 PGO, 32 位版在 54 版中启用。
  • AutoFDO 进行了 PGO 的优化,直接将某数据中心中的 C/C++ 程序的性能提高了 5-15%(不用改业务代码)。

Go 怎么读取 PGO

PGO 第一个版本将会先支持 pprof CPU,直接读取 pprof CPU profile 文件来完成优化。

有以下两种方式:

  • 手动指定:Go 工具链在 go build 子命令增加了 -pgo=<path> 参数,用于显式指定用于 PGO 构建的 profile 文件位置。
  • 自动指定:当 Go 工具链在主模块目录下找到 default.pgo 的配置文件时,将会自动启用 PGO。

快速 Demo

初始化应用程序

首先我们创建一个 Demo 目录,用于做一系列的实验。执行如下命令:

$ mkdir pgo-demo && cd pgo-demo

初始化模块路径和拉取程序所需的依赖:

$ go mod init example.com/markdown
go: creating new go.mod: module example.com/markdown

$ go get gitlab.com/golang-commonmark/markdown@bf3e522c626a

创建 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 build -o markdown.nopgo
$ ./markdown.nopgo 
2023/10/02 13:55:40 Serving on port 8080...

运行起来后进行验证,这是一个将 Markdown 转换为 HTML 的应用程序。

我们从 GitHub 上拉取一份 markdown 文件并给到该程序进行转换。如下命令:

$ curl -o README.md -L "https://raw.githubusercontent.com/golang/go/c16c2c49e2fa98ae551fc6335215fadd62d33542/README.md"
$ curl --data-binary @README.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>
...

如果正常则说明运行没问题。

收集 PGO 所需的配置文件

一般情况下,我们可以通过生产、测试环境的 pprof 采集所需的 profile 文件,用于做 PGO 的配置文件。

但由于示例没有对应的生产环境。本次快速 Demo,Go 官方提供了一个简单的程序来快速的发压。

在确保前面小节的 pgo-demo 程序正常运行的情况下。运行如下命令,启动一个发压程序:

$ go run github.com/prattmic/markdown-pgo/load@latest

收集对应的 profile 文件:

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

生成了一个 cpu.pprof 文件,可以在后续使用。

应用程序使用 PGO

前面我们有提到,当模块目录下包含 default.pgo 时。Go 工具链就会自动应用 PGO

我们只需要将前面的 cpu.pprof 修改一下即可。执行如下命令:

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

编译成功后,使用如下命令验证是否正常:

$ go version -m markdown.withpgo
markdown.withpgo: go1.21.1
    path    example.com/markdown
    mod    example.com/markdown    (devel)    
    ...
    build    GOOS=darwin
    build    GOAMD64=v1
    build    -pgo=/Users/eddycjy/app/go/pgo-demo/default.pgo

可以看到最后的 build -pgo=...,代表该应用程序成功应用了我们的 default.pgo 文件(启用 PGO)。

总结

PGO 作为 Go 新版本的一个性能好帮手,在官方给出的数据中启用 PGO 后,性能能够得到一定的提升。但也会带来其他方面(CPU、大小等)的开销增加。

如本文的例子中,官方给出的数据是程序性能提升了约 2~4%,CPU 使用率会带来 2~7% 的开销增加。也可能会导致构建时长变长一些、编译后的二进制文件会稍微大一些。

面对一些场景,PGO 是一个不错的性能优化方式。但有利必有弊,就看这个应用程序的类型和综合取舍了。

文章持续更新,可以微信搜【脑子进煎鱼了】阅读,本文 GitHub github.com/eddycjy/blog 已收录,学习 Go 语言可以看 Go 学习地图和路线,欢迎 Star 催更。

Go 图书系列

推荐阅读


煎鱼
8.4k 声望12.8k 粉丝