译 | 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
77 0
|
Java Go
译 | Profiling Go Programs(三)
译 | Profiling Go Programs(三)
52 0
|
Java 编译器 Go
译 | Profiling Go Programs(二)
译 | Profiling Go Programs(二)
101 0
|
2月前
|
存储 监控 算法
员工上网行为监控中的Go语言算法:布隆过滤器的应用
在信息化高速发展的时代,企业上网行为监管至关重要。布隆过滤器作为一种高效、节省空间的概率性数据结构,适用于大规模URL查询与匹配,是实现精准上网行为管理的理想选择。本文探讨了布隆过滤器的原理及其优缺点,并展示了如何使用Go语言实现该算法,以提升企业网络管理效率和安全性。尽管存在误报等局限性,但合理配置下,布隆过滤器为企业提供了经济有效的解决方案。
85 8
员工上网行为监控中的Go语言算法:布隆过滤器的应用
|
2月前
|
存储 Go 索引
go语言中数组和切片
go语言中数组和切片
47 7
|
2月前
|
Go 开发工具
百炼-千问模型通过openai接口构建assistant 等 go语言
由于阿里百炼平台通义千问大模型没有完善的go语言兼容openapi示例,并且官方答复assistant是不兼容openapi sdk的。 实际使用中发现是能够支持的,所以自己写了一个demo test示例,给大家做一个参考。
|
2月前
|
程序员 Go
go语言中结构体(Struct)
go语言中结构体(Struct)
116 71
|
2月前
|
存储 Go 索引
go语言中的数组(Array)
go语言中的数组(Array)
117 67
|
9天前
|
存储 监控 算法
内网监控系统之 Go 语言布隆过滤器算法深度剖析
在数字化时代,内网监控系统对企业和组织的信息安全至关重要。布隆过滤器(Bloom Filter)作为一种高效的数据结构,能够快速判断元素是否存在于集合中,适用于内网监控中的恶意IP和违规域名筛选。本文介绍其原理、优势及Go语言实现,提升系统性能与响应速度,保障信息安全。
22 5
|
18天前
|
算法 安全 Go
Go语言中的加密和解密是如何实现的?
Go语言通过标准库中的`crypto`包提供丰富的加密和解密功能,包括对称加密(如AES)、非对称加密(如RSA、ECDSA)及散列函数(如SHA256)。`encoding/base64`包则用于Base64编码与解码。开发者可根据需求选择合适的算法和密钥,使用这些包进行加密操作。示例代码展示了如何使用`crypto/aes`包实现对称加密。加密和解密操作涉及敏感数据处理,需格外注意安全性。
38 14