假设你手上有个Go语言编写的程序,你打算提升它的性能。目前有一些工具可以为此提供帮助。这些工具能帮你发现包括CPU、IO和内存在内多种类型的热点。所谓热点,是指那些为了能显著提升性能而值得你去关注的地方。有时候这些工具还能帮助你发现程序中主要的性能瑕疵。举个例子,你没必要每次执行SQL查询前都对SQL语句进行参数化解析,你可以将这个准备过程在程序启动时一次完成。再举个例子,当前某个算法的复杂度是O(N²),但其实存在一个复杂度是O(N)的解决方案。为了能发现这些问题,需要理智地检查你在优化分析器中获取到的信息。比如上面提到的第一个问题,你会注意到相当长的时间被花费在了对SQL语句的准备上。
了解针对性能的不同边界因素也是比较重要的。比方说,如果一个程序使用100 Mbps带宽的网络进行通信,而目前已经占用了超过90 Mbps的带宽,为了提升它的性能,你拿这样的程序也没啥办法了。在磁盘IO、内存消耗和计算密集型任务方面,也有类似的边界因素。
将这点牢记在心,让我们看看有哪些工具可以用。
注意:这些工具会彼此互相影响。例如,对内存使用优化分析器会导致针对CPU的优化分析器产生误差,对goroutine阻塞使用优化分析器会影响调度器跟踪等等。为了获得更加精确的信息,请在隔离的环境中使用这些工具。
注意:本文描述的用法基于Go语言发布的1.3版。
Go 运行时包含了内建的CPU分析器,它用来展示某个函数耗费了多少CPU百分时间。这里有三种方式来使用它:
1. 最简单的是用"go test"的-cpuprofile选项。例如下面的命令:
$ go test -run=none -bench=ClientServerParallel4 -cpuprofile=cprof net/http
将会分析所给的基准并将结果写入"cprof"文件中。
然后:
$ go tool pprof --text http.test cprof
将会打印耗费最多CPU时间的函数列表。
这里有几种可用的输出形式,最实用的有 --text, --web 和 --list。运行 "go tool pprof" 来得到完整的列表。
这个选项最明显的缺点是它只能用来做测试。
2. net/http/pprof 包。这是网络服务器的理想解决方案。你可能仅仅需要导入net/http/pprof,然后使用下面的方法收集分析结果:
$ go tool pprof --text mybin http://myserver:6060:/debug/pprof/profile
3. 手动收集. 你需要导入 runtime/pprof 然后再main函数中添加下面的代码:
if *flagCpuprofile != "" { f, err := os.Create(*flagCpuprofile) if err != nil { log.Fatal(err) } pprof.StartCPUProfile(f) defer pprof.StopCPUProfile() }
分析结果会被写入指定的文件中,像第一种方式一样使之可视化。
这里有一个使用 --web 选项来实现可视化的例子:
你可以使用--list=funcname来审查单一函数。例如,下面的结果显示了附加函数中的时间流逝:
. . 93: func (bp *buffer) WriteRune(r rune) error { . . 94: if r < utf8.RuneSelf { 5 5 95: *bp = append(*bp, byte(r)) . . 96: return nil . . 97: } . . 98: . . 99: b := *bp . . 100: n := len(b) . . 101: for n+utf8.UTFMax > cap(b) { . . 102: b = append(b, 0) . . 103: } . . 104: w := utf8.EncodeRune(b[n:n+utf8.UTFMax], r) . . 105: *bp = b[:n+w] . . 106: return nil . . 107: }
你可以在这里找到pprof工具的详细信息以及上图中数字的描述。
在3种特殊的情形下分析器不能解开堆栈:GC,System和ExternalCode。GC 表示垃圾回收期间的用时,查看下面的内存分析器和垃圾回收跟踪器以得到优化建议。System 表示goroutine调度程序,栈管理代码和其他辅助运行时代码的用时。ExternalCode 表示本地动态库耗时。
对于你在简介中看到的东西的解释,这里有一些提示和技巧。
如果你看到大量的时间消耗在运行时间,内存分配的函数,那么暗示程序产生了大量过度的小内存分配工作。此描述将会告诉你这些分配来自哪里。查看内存分析器部分可以获得如何优化这种情况的建议。可考虑对程序进行重新调整以消除频繁对共享资源的获取和接入。对此,一般的解决技术方案包括有分片/分区,本地缓存/计量和写时拷贝。
如果大量的时间消耗在频道操作,同步。互斥代码和其他同步原语或者系统容器,那么程序很可能正在饱受资源争夺的痛苦。
如果大量的时间消耗在系统调用的读/写,那么暗示程序产生了大量过度的小块读写。对这种情况,围绕系统文件或者网络连接而包装的Bufio会很有帮助。
如果大量的时间消耗在GC容器,那么程序要么分配了大量的短暂临时的对象,要么堆栈的空间非常小以致垃圾回收收集变得非常频繁。通过查看垃圾收集追踪器和内存分析器这两部分可以得到一些优化的建议。
温馨提示:当前CPU分析器不能工作于darwin。
温馨提示:在windows系统上你需要安装Cygwin, Perl和Graphviz才能生成svg/web简介。
温馨提示:在linux系统上你也可以尝试PERF系统分析器。它不能解开Go的栈,但它可以获得cgo或者SWIG的代码和kernel内核的快照并解开。所以它对于洞悉本地/kernel内核的性能瓶颈非常有帮助。
内存分析器展示了哪些函数申请了堆内存。你可以通过熟悉的途径来收集这些信息,一如使用CPU分析器:和 'go test --memprofile', 以及通过 http://myserver:6060:/debug/pprof/heap的net/http/pprof 或者通过调用runtime/pprof.WriteHeapProfile。
你仅仅可以可视化描述收集器当前时间内的申请(默认下--inuse_space标识指向pprof),或者自程序启动以来全部的申请(--alloc_space标识指向pprof)。前者对于在当前活动的程序通过net/http/pprof收集描述很有帮助,而后者则对在程序后端(否则你将会看到的几乎都是空的描述)收集描述有帮助。
温馨提示:内存分析器采取抽样的方式,也就是说,它仅仅从一些内存分配的子集中收集信息。有可能对一个对象的采样与被采样对象的大小成比例。你可以通过使用go test --memprofilerate标识,或者通过程序启动时 的运行配置中的MemProfileRate变量来改变调整这个采样的比例。如果比例为1,则会导致全部申请的信息都会被收集,但是这样的话将会使得执行变慢。默认的采样比例是每512KB的内存申请就采样一次。
你同样可以将分配的字节数或者分配的对象数形象化(分别是以--inuse/alloc_space和--inuse/alloc_objects为标志)。分析器倾向于在性能分析中对较大的对象采样。但是需要注意的是大的对象会影响内存消耗和垃圾回收时间,大量的小的内存分配会影响运行速度(某种程度上也会影响垃圾回收时间)。所以最好同时考虑它们。
对象可以是持续的也可以是瞬时的。如果你在程序开始的时候需要分配几个大的持续对象,它们很有可能能被分析器取样(因为它们比较大)这些对象会影响内存消耗量和垃圾回收时间,但它们不会影响正常的运行速度(在它们上没有内存管理操作)。另一方面,如果你有大量持续期很短的对象,它们几乎不会表现在曲线中(如果你使用默认的--inuse_space模式)。但它们的确显著影响运行速度,因为它们被不断地分配和释放。所以再说一遍,最好同时考虑这两种类型的对象。
所以,大体上,如果你想减小内存消耗量,那么你需要查看程序正常运行时--inuse_space收集的概要。如果你想提升程序的运行速度,就要查看在程序特征运行时间后或程序结束之后--alloc_objects收集的概要。
报告间隔时间由几个标志控制,--functions让pprof报告在函数等级(默认)。--lines使pprof报告基于代码行等级,如果关键函数分布在不同的代码行上,这将变得很有用。同样还有--addresses和--files选项, 分别定位到精确的指令地址等级和文件等级。
还有一个对内存概要很有用的选项,你可以直接在浏览器中查看它(需要你导入net/http/pprof包)。你打开http://myserver:6060/debug/pprof/heap?debug=1就会看到堆概要,如下:
heap profile: 4: 266528 [123: 11284472] @ heap/1048576 1: 262144 [4: 376832] @ 0x28d9f 0x2a201 0x2a28a 0x2624d 0x26188 0x94ca3 0x94a0b 0x17add6 0x17ae9f 0x1069d3 0xfe911 0xf0a3e 0xf0d22 0x21a70 # 0x2a201 cnew+0xc1 runtime/malloc.goc:718 # 0x2a28a runtime.cnewarray+0x3a runtime/malloc.goc:731 # 0x2624d makeslice1+0x4d runtime/slice.c:57 # 0x26188 runtime.makeslice+0x98 runtime/slice.c:38 # 0x94ca3 bytes.makeSlice+0x63 bytes/buffer.go:191 # 0x94a0b bytes.(*Buffer).ReadFrom+0xcb bytes/buffer.go:163 # 0x17add6 io/ioutil.readAll+0x156 io/ioutil/ioutil.go:32 # 0x17ae9f io/ioutil.ReadAll+0x3f io/ioutil/ioutil.go:41 # 0x1069d3 godoc/vfs.ReadFile+0x133 godoc/vfs/vfs.go:44 # 0xfe911 godoc.func·023+0x471 godoc/meta.go:80 # 0xf0a3e godoc.(*Corpus).updateMetadata+0x9e godoc/meta.go:101 # 0xf0d22 godoc.(*Corpus).refreshMetadataLoop+0x42 godoc/meta.go:141 2: 4096 [2: 4096] @ 0x28d9f 0x29059 0x1d252 0x1d450 0x106993 0xf1225 0xe1489 0xfbcad 0x21a70 # 0x1d252 newdefer+0x112 runtime/panic.c:49 # 0x1d450 runtime.deferproc+0x10 runtime/panic.c:132 # 0x106993 godoc/vfs.ReadFile+0xf3 godoc/vfs/vfs.go:43 # 0xf1225 godoc.(*Corpus).parseFile+0x75 godoc/parser.go:20 # 0xe1489 godoc.(*treeBuilder).newDirTree+0x8e9 godoc/dirtrees.go:108 # 0xfbcad godoc.func·002+0x15d godoc/dirtrees.go:100
每个条目开头的数字("1: 262144 [4: 376832]")分别表示目前存活的对象,存活对象占据的内存, 分配对象的个数和所有分配对象占据的内存总量。
优化工作经常和特定应用程序相关,但也有一些普遍建议。
1. 将小对象组合成大对象。比如, 将 *bytes.Buffer 结构体成员替换为bytes。缓冲区 (你可以预分配然后通过调用bytes.Buffer.Grow为写做准备) 。这将减少很多内存分配(更快)并且减缓垃圾回收器的压力(更快的垃圾回收) 。
2. 离开声明作用域的局部变量促进堆分配。编译器不能保证这些变量拥有相同的生命周期,因此为他们分别分配空间。所以你也可以对局部变量使用上述的建议。比如:将
for k, v := range m { k, v := k, v // copy for capturing by the goroutine go func() { // use k and v }() }
替换为:
for k, v := range m { x := struct{ k, v string }{k, v} // copy for capturing by the goroutine go func() { // use x.k and x.v }() }
这就将两次内存分配替换为了一次。然而,这样的优化方式会影响代码的可读性,因此要合理地使用它。
3. 组合内存分配的一个特殊情形是分片数组预分配。如果你清楚一个特定的分片的大小,你可以给末尾数组进行预分配:
type X struct { buf []byte bufArray [16]byte // Buf usually does not grow beyond 16 bytes. } func MakeX() *X { x := &X{} // Preinitialize buf with the backing array. x.buf = x.bufArray[:0] return x }
4. 尽可能使用小数据类型。比如用int8代替int。
评论删除后,数据将无法恢复
评论(15)
引用来自“yak”的评论
golang的gc相比java的gc有什么优势吗?引用来自“OSC首席键客”的评论
好像没有没优势吧!看到有文章说算法是原始的,别人都不用的算法。听说要改吧!引用来自“root_root”的评论
golang的gc还在完善中呢,每个版本都在改。。 比不上java引用来自“Raynar1”的评论
这一个golang的gc和java的gc一比就是狗屎,说他是狗屎其实算是侮辱狗屎,嗯引用来自“Raynar1”的评论
这一个golang的gc和java的gc一比就是狗屎,说他是狗屎其实算是侮辱狗屎,嗯引用来自“yak”的评论
golang的gc相比java的gc有什么优势吗?引用来自“yak”的评论
golang的gc相比java的gc有什么优势吗?引用来自“OSC首席键客”的评论
好像没有没优势吧!看到有文章说算法是原始的,别人都不用的算法。听说要改吧!引用来自“yak”的评论
golang的gc相比java的gc有什么优势吗?