译 | 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(三)
52 0
|
Java 编译器 Go
译 | Profiling Go Programs(二)
译 | Profiling Go Programs(二)
101 0
|
算法 Ubuntu Java
译 | Profiling Go Programs(一)
译 | Profiling Go Programs
112 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