译 | Profiling Go Programs(四)

简介: 译 | Profiling Go Programs

我们现在可以轻松地遵循粗箭头,看看 FindLoops 是否触发了大部分垃圾收集。 如果我们列出 FindLoops 们可以看到,大部分在开始时是正确的:

(pprof) list FindLoops
...
     .      .  270: func FindLoops(cfgraph *CFG, lsgraph *LSG) {
     .      .  271:     if cfgraph.Start == nil {
     .      .  272:             return
     .      .  273:     }
     .      .  274:
     .      .  275:     size := cfgraph.NumNodes()
     .      .  276:
     .    145  277:     nonBackPreds := make([][]int, size)
     .      9  278:     backPreds := make([][]int, size)
     .      .  279:
     .      1  280:     number := make([]int, size)
     .     17  281:     header := make([]int, size, size)
     .      .  282:     types := make([]int, size, size)
     .      .  283:     last := make([]int, size, size)
     .      .  284:     nodes := make([]*UnionFindNode, size, size)
     .      .  285:
     .      .  286:     for i := 0; i < size; i++ {
     2     79  287:             nodes[i] = new(UnionFindNode)
     .      .  288:     }
...
(pprof)

每次调用 FindLoops ,它都会分配一些相当大的 bookkeeping structures。 由于benchmark调用 FindLoops 50次,因此这些增加了大量的垃圾,所以垃圾收集器的工作量很大。

使用垃圾收集语言并不意味着您可以忽略内存分配问题。 在这种情况下,一个简单的解决方案是引入一个缓存,以便每次调用 FindLoops 时尽可能重用前一个调用的存储。

事实上,在Hundt的论文中,他解释说Java程序只需要进行这种改变就可以得到合理的性能,但是他没有在其他垃圾收集的实现中做出相同的改变。

我们将添加一个全局cache结构:

var cache struct {
    size int
    nonBackPreds [][]int
    backPreds [][]int
    number []int
    header []int
    types []int
    last []int
    nodes []*UnionFindNode
}

然后把它当作 FindLoops 内存分配的替代品:

if cache.size < size {
    cache.size = size
    cache.nonBackPreds = make([][]int, size)
    cache.backPreds = make([][]int, size)
    cache.number = make([]int, size)
    cache.header = make([]int, size)
    cache.types = make([]int, size)
    cache.last = make([]int, size)
    cache.nodes = make([]*UnionFindNode, size)
    for i := range cache.nodes {
        cache.nodes[i] = new(UnionFindNode)
    }
}
nonBackPreds := cache.nonBackPreds[:size]
for i := range nonBackPreds {
    nonBackPreds[i] = nonBackPreds[i][:0]
}
backPreds := cache.backPreds[:size]
for i := range nonBackPreds {
    backPreds[i] = backPreds[i][:0]
}
number := cache.number[:size]
header := cache.header[:size]
types := cache.types[:size]
last := cache.last[:size]
nodes := cache.nodes[:size]

当然,这样的全局变量是糟糕的工程实践:它意味着对 FindLoops 并发调用现在是不安全的。 目前,我们正在进行尽可能少的更改,以便了解对我们的程序的性能有什么重要意义; 这种变化很简单,并且反映了Java实现中的代码。 Go程序的最终版本将使用单独的 LoopFinder 实例来跟踪此内存,从而恢复并发使用的可能性。

$ make havlak5
go build havlak5.go
$ ./xtime ./havlak5
# of loops: 76000 (including 1 artificial root node)
8.03u 0.06s 8.11r 770352kB ./havlak5
$

diff from havlak4

总结

我们可以做更多的事情来清理程序并使其更快,但是它们都不需要我们尚未展示的分析技术。 内部循环中使用的工作列表可以跨迭代和跨调用进行重用。 FindLoops,它可以与在该过程中生成的单独的“节点池”相结合。 类似地,循环图存储可以在每次迭代时重用,而不是重新分配。 除了这些性能变化之外, 最终版本是使用惯用的Go样式编写的,使用数据结构和方法。 风格变化对运行时间的影响很小:算法和约束不变。

最终版本运行2.29秒,使用351 MB内存:

$ make havlak6
go build havlak6.go
$ ./xtime ./havlak6
# of loops: 76000 (including 1 artificial root node)
2.26u 0.02s 2.29r 360224kB ./havlak6
$

这比我们开始的程序快11倍。 即使我们禁用对生成的循环图的重用,以便唯一的缓存内存是循环查找bookeeping,程序仍然比原始运行速度快6.7倍,并且使用的内存减少1.5倍。

$ ./xtime ./havlak6 -reuseloopgraph=false
# of loops: 76000 (including 1 artificial root node)
3.69u 0.06s 3.76r 797120kB ./havlak6 -reuseloopgraph=false
$

当然,将这个Go程序与原始的C++程序进行比较是不公平的,因为它使用了低效的数据结构,例如 sets 其中 vectors 更合适。 作为完整性检查,我们将最终的Go程序翻译成等效的C++代码。 它的执行时间类似于Go程序:

$ make havlak6cc
g++ -O3 -o havlak6cc havlak6.cc
$ ./xtime ./havlak6cc
# of loops: 76000 (including 1 artificial root node)
1.99u 0.19s 2.19r 387936kB ./havlak6cc

Go程序的运行速度几乎和C++程序一样快。 由于C++程序使用自动删除和分配而不是显式缓存,因此C++程序更短更容易编写,但不是那么明显:

$ wc havlak6.cc; wc havlak6.go
 401 1220 9040 havlak6.cc
 461 1441 9467 havlak6.go
$

havlak6.cchavlak6.go

Benchmarks与他们测量的程序一样好。 我们使用 go tool pprof 来研究低效的Go程序,然后将其性能提高一个数量级,并将其内存使用量减少3.7倍。 随后与等效优化的C++程序进行比较表明,当程序员小心内循环生成多少垃圾时,Go可以与C++竞争。

用于编写这篇文章的程序源代码,Linux x86-64二进制文件和配置文件可以在GitHub上的benchgraffiti项目中找到 。

如上所述, go test 已经包含了这些 profiling flags:benchmark function ,你就完成了。 还有一个用于 profiling 数据的标准HTTP接口。 在HTTP服务器中,添加

import _ "net/http/pprof"

将安装几个URL的处理程序 /debug/pprof/. 然后,您可以用一个参数运行 go tool pprof——指向服务器profiling数据的URL,它将下载并检查实时Profile文件。

go tool pprof http://localhost:6060/debug/pprof/profile   # 30-second CPU profile
go tool pprof http://localhost:6060/debug/pprof/heap      # heap profile
go tool pprof http://localhost:6060/debug/pprof/block     # goroutine blocking profile

goroutine blocking profile 将在以后的文章中解释。 敬请关注。

作者:Russ Cox,2011年7月; 由Shenghou Ma更新,2013年5月

原文:https://blog.golang.org/profiling-go-programs


源代码:https://github.com/cyningsun/go-test

本文作者 : cyningsun

本文地址https://www.cyningsun.com/07-20-2019/profiling-go-programs-cn.html

版权声明 :本博客所有文章除特别声明外,均采用 CC BY-NC-ND 3.0 CN 许可协议。转载请注明出处!

# Golang

  1. 译|There Are No Reference Types in Go
  2. Go 语言没有引用类型,指针也与众不同
  3. 译|What “accept interfaces, return structs” means in Go
  4. 如何用好 Go interface
  5. 一个优雅的 LRU 缓存实现
目录
相关文章
|
Java Go
译 | Profiling Go Programs(三)
译 | Profiling Go Programs(三)
44 0
|
Java 编译器 Go
译 | Profiling Go Programs(二)
译 | Profiling Go Programs(二)
91 0
|
算法 Ubuntu Java
译 | Profiling Go Programs(一)
译 | Profiling Go Programs
99 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
|
10天前
|
Ubuntu 编译器 Linux
go语言中SQLite3驱动安装
【11月更文挑战第2天】
32 7
|
10天前
|
关系型数据库 Go 网络安全
go语言中PostgreSQL驱动安装
【11月更文挑战第2天】
39 5
|
9天前
|
安全 Go
用 Zap 轻松搞定 Go 语言中的结构化日志
在现代应用程序开发中,日志记录至关重要。Go 语言中有许多日志库,而 Zap 因其高性能和灵活性脱颖而出。本文详细介绍如何在 Go 项目中使用 Zap 进行结构化日志记录,并展示如何定制日志输出,满足生产环境需求。通过基础示例、SugaredLogger 的便捷使用以及自定义日志配置,帮助你在实际开发中高效管理日志。
26 1
|
9天前
|
程序员 Go
go语言中的控制结构
【11月更文挑战第3天】
85 58