译 | Profiling Go Programs(一)

简介: 译 | Profiling Go Programs

背景

在Scala Days 2011,Robert Hundt 发表了一篇名为 Loop Recognition in C++/Java/Go/Scala 的论文。 该论文实现了一种特定的循环查找算法,例如您可以在C++,Go,Java,Scala编译器的流分析传递中使用,然后使用这些程序得到这些语言中典型性能问题的结论。该论文中的Go程序运行速度非常慢,使它成为演示的绝佳机会:如何使用Go的分析工具来实现慢速程序并使其更快。

通过使用go的分析工具来识别和纠正特定的瓶颈,可以使go loop finding程序运行速度提高一个数量级,并减少6倍的内存。

更新:由于最近gcc中libstdc++优化,现在内存减少是3.7倍。

Hundt的论文没有说明他使用的C++,Go,Java和Scala工具的版本。 在这篇博文中,我们将使用6g Go编译器的最新每周快照以及Ubuntu Natty发行版附带的g++版本。(我们不会使用Java或Scala,因为我们不擅长用这两种语言编写高效的程序,所以这种比较是不公平的。因为C++是论文中最快的语言,所以这里与C++的比较应该足够了。)

更新:在这篇更新的帖子中,我们将使用amd64上Go编译器的最新开发快照以及2013年3月发布的最新版g++ - 4.8.0。

$ go version
go version devel +08d20469cc20 Tue Mar 26 08:27:18 2013 +0100 linux/amd64
$ g++ --version
g++ (GCC) 4.8.0
Copyright (C) 2013 Free Software Foundation, Inc.
...
$

程序运行在配备3.4GHz Core i7-2600 CPU 16 GB RAM的Gentoo Linux’s 3.8.4-gentoo内核的计算机上。 通过以下代码禁用了机器的CPU频率调整:

$ sudo bash
# for i in /sys/devices/system/cpu/cpu[0-7]
do
    echo performance > $i/cpufreq/scaling_governor
done
#

我们采用 Hundt’s benchmark programs 的C++和Go部分,将每个程序组合成一个单独的源文件,并删掉了除一行输出之外的所有内容。我们将使用Linux的time工具为程序计时 ,其格式显示为 user time, system time, real time, and maximum memory usage:

Mac OS 需要 brew install gnu-time,使用 gtime 命令替代

$ cat xtime
#!/bin/sh
/usr/bin/time -f '%Uu %Ss %er %MkB %C' "$@"
$
$ make havlak1cc
g++ -O3 -o havlak1cc havlak1.cc
$ ./xtime ./havlak1cc
# of loops: 76002 (total 3800100)
loop-0, nest: 0, depth: 0
17.70u 0.05s 17.80r 715472kB ./havlak1cc
$
$ make havlak1
go build havlak1.go
$ ./xtime ./havlak1
# of loops: 76000 (including 1 artificial root node)
25.05u 0.11s 25.20r 1334032kB ./havlak1
$

C++程序运行使用17.80秒 700 MB内存。 Go程序运行使用25.20秒 1302 MB的内存。(该结果很难与论文中的统一,但本文的重点是探索如何使用 go tool pprof,而不是重现论文的结果。)

CPU Profile

要调优Go程序,我们必须启用profiling。 如果代码使用Go testing package的benchmarking分析,可以使用gotest标准的 -cpuprofile-memprofile flags 。 我们必须在程序中导入 runtime/pprof 并添加几行代码:

var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
func main() {
    flag.Parse()
    if *cpuprofile != "" {
        f, err := os.Create(*cpuprofile)
        if err != nil {
            log.Fatal(err)
        }
        pprof.StartCPUProfile(f)
        defer pprof.StopCPUProfile()
    }
    ...

新代码定义了一个名为 cpuprofile 的flag,调用 Go flag library 来解析命令行flags,如果在命令行上设置了 cpuprofile flag,则 starts CPU profiling 重定向到该文件。 在程序退出之前,profiler 需要最后调用 StopCPUProfile 来flush未写入文件的数据; 我们使用 defer 来确保在 main 返回前执行该调用。

添加该代码后,我们可以使用新的 -cpuprofile flag 运行程序,然后运行 go tool pprof 来解释 profile。

$ make havlak1.prof
./havlak1 -cpuprofile=havlak1.prof
# of loops: 76000 (including 1 artificial root node)
$ go tool pprof havlak1 havlak1.prof
Welcome to pprof!  For help, type 'help'.
(pprof)

go tool pprof 程序是 Google’s pprof C++ profiler 的细微变体。最重要的命令是 topN ,显示profile中 top N 的样本:

(pprof) top10
Total: 2525 samples
     298  11.8%  11.8%      345  13.7% runtime.mapaccess1_fast64
     268  10.6%  22.4%     2124  84.1% main.FindLoops
     251   9.9%  32.4%      451  17.9% scanblock
     178   7.0%  39.4%      351  13.9% hash_insert
     131   5.2%  44.6%      158   6.3% sweepspan
     119   4.7%  49.3%      350  13.9% main.DFS
      96   3.8%  53.1%       98   3.9% flushptrbuf
      95   3.8%  56.9%       95   3.8% runtime.aeshash64
      95   3.8%  60.6%      101   4.0% runtime.settype_flush
      88   3.5%  64.1%      988  39.1% runtime.mallocgc

CPU profiling 启用时,Go程序每秒大约停止100次,并在当前正在执行的goroutine堆栈上记录由程序计数器组成的样本。 该 profile 有2525个样本,因此运行时间超过25秒。 在 go tool pprof 输出中,样本中出现的每个函数都有一行。

  • 前两列显示函数运行的样本数(而不是等待被调用函数返回),展示为原始计数和总样本的百分比。 runtime.mapaccess1_fast64 函数在298个样本期间运行(11.8%)。 top10 输出按样本计数排序。
  • 第三列显示了清单期间的运行总数:前三行占样本的32.4%。
  • 第四和第五列显示函数出现的样本数(运行或等待被调用的函数返回)。 main.FindLoops函数在10.6%的样本中运行,但是它在84.1%的样本中位于调用堆栈(它或它的调用正在运行)上。

要按第四和第五列排序,要使用 -cum(for cumulative)flag:

(pprof) top5 -cum
Total: 2525 samples
       0   0.0%   0.0%     2144  84.9% gosched0
       0   0.0%   0.0%     2144  84.9% main.main
       0   0.0%   0.0%     2144  84.9% runtime.main
       0   0.0%   0.0%     2124  84.1% main.FindHavlakLoops
     268  10.6%  10.6%     2124  84.1% main.FindLoops
(pprof) top5 -cum

事实上, main.FindLoopsmain.main 的总数应该是100%,但每个堆栈样本只包含底部的100个堆栈帧; 在大约四分之一的样本中,递归 main.DFS 函数比 main.main 深100多帧,因此完整的trace被截断。

stack trace样本包含有关函数调用关系,不是文本列表可以显示的有趣数据。 web 命令以SVG格式输出profile数据为图像,并在Web浏览器中打开它。 (还有一个 gv 命令可以编写PostScript并在Ghostview中打开它。两个命令都需要安装 graphviz 。)

(pprof) web

完整图形的一小部分如下:

图中的每个框对应于一个函数,并且框根据函数运行的样本数来确定大小。 从方框X到方框Y的线表示X调用Y; 沿线的数字是调用在样本中出现的次数。 如果在单个样本中多次出现呼叫,例如在递归函数调用期间,则每次出现都会计入线权重。 解释了从 main.DFS 到自身的线的上的权重:21342。

目录
相关文章
|
存储 缓存 算法
译 | Profiling Go Programs(四)
译 | Profiling Go Programs
71 0
|
Java Go
译 | Profiling Go Programs(三)
译 | Profiling Go Programs(三)
44 0
|
Java 编译器 Go
译 | Profiling Go Programs(二)
译 | Profiling Go Programs(二)
91 0
|
6天前
|
存储 JSON 监控
Viper,一个Go语言配置管理神器!
Viper 是一个功能强大的 Go 语言配置管理库,支持从多种来源读取配置,包括文件、环境变量、远程配置中心等。本文详细介绍了 Viper 的核心特性和使用方法,包括从本地 YAML 文件和 Consul 远程配置中心读取配置的示例。Viper 的多来源配置、动态配置和轻松集成特性使其成为管理复杂应用配置的理想选择。
23 2
|
5天前
|
Go 索引
go语言中的循环语句
【11月更文挑战第4天】
13 2
|
5天前
|
Go C++
go语言中的条件语句
【11月更文挑战第4天】
16 2
|
9天前
|
Ubuntu 编译器 Linux
go语言中SQLite3驱动安装
【11月更文挑战第2天】
31 7
|
10天前
|
关系型数据库 Go 网络安全
go语言中PostgreSQL驱动安装
【11月更文挑战第2天】
39 5
|
9天前
|
安全 Go
用 Zap 轻松搞定 Go 语言中的结构化日志
在现代应用程序开发中,日志记录至关重要。Go 语言中有许多日志库,而 Zap 因其高性能和灵活性脱颖而出。本文详细介绍如何在 Go 项目中使用 Zap 进行结构化日志记录,并展示如何定制日志输出,满足生产环境需求。通过基础示例、SugaredLogger 的便捷使用以及自定义日志配置,帮助你在实际开发中高效管理日志。
26 1
|
8天前
|
程序员 Go
go语言中的控制结构
【11月更文挑战第3天】
85 58