要查找内存分配,我们可以列出这些函数。
(pprof) list FindLoops Total: 82.4 MB ROUTINE ====================== main.FindLoops in /home/rsc/g/benchgraffiti/havlak/havlak3.go 56.3 56.3 Total MB (flat / cumulative) ... 1.9 1.9 268: nonBackPreds := make([]map[int]bool, size) 5.8 5.8 269: backPreds := make([][]int, size) . . 270: 1.9 1.9 271: number := make([]int, size) 1.9 1.9 272: header := make([]int, size, size) 1.9 1.9 273: types := make([]int, size, size) 1.9 1.9 274: last := make([]int, size, size) 1.9 1.9 275: nodes := make([]*UnionFindNode, size, size) . . 276: . . 277: for i := 0; i < size; i++ { 9.5 9.5 278: nodes[i] = new(UnionFindNode) . . 279: } ... . . 286: for i, bb := range cfgraph.Blocks { . . 287: number[bb.Name] = unvisited 29.5 29.5 288: nonBackPreds[i] = make(map[int]bool) . . 289: } ...
看起来当前的瓶颈与上一个瓶颈相同:在原本使用更简单的数据结构就足够时使用 map。 FindLoops
正在分配大约29.5 MB的 maps。
顺便说一句,如果我们使用--inuse_objects
flag 运行 go tool pprof
,它将报告分配计数而不是大小:
$ go tool pprof --inuse_objects havlak3 havlak3.mprof Adjusting heap profiles for 1-in-524288 sampling rate Welcome to pprof! For help, type 'help'. (pprof) list FindLoops Total: 1763108 objects ROUTINE ====================== main.FindLoops in /home/rsc/g/benchgraffiti/havlak/havlak3.go 720903 720903 Total objects (flat / cumulative) ... . . 277: for i := 0; i < size; i++ { 311296 311296 278: nodes[i] = new(UnionFindNode) . . 279: } . . 280: . . 281: // Step a: . . 282: // - initialize all nodes as unvisited. . . 283: // - depth-first traversal and numbering. . . 284: // - unreached BB's are marked as dead. . . 285: // . . 286: for i, bb := range cfgraph.Blocks { . . 287: number[bb.Name] = unvisited 409600 409600 288: nonBackPreds[i] = make(map[int]bool) . . 289: } ... (pprof)
由于~200,000个 maps 占29.5 MB,因此初始 map 分配大约需要150个字节。 map 用于保存键值对是合理的,但是在此处map被用作简单集合的替代是不合理的。
我们可以使用简单的slice来列出元素,而不是使用map。 在使用 map 的所有情况中,一种情况要除外,map不可能插入重复的元素。在下面的的一个例子中,我们可以编写一个简单的“append”内置函数变体:
func appendUnique(a []int, x int) []int { for _, y := range a { if x == y { return a } } return append(a, x) }
除了编写该函数之外,将Go程序更改为使用slices而不是maps需要更改几行代码。
$ make havlak4 go build havlak4.go $ ./xtime ./havlak4 # of loops: 76000 (including 1 artificial root node) 11.84u 0.08s 11.94r 810416kB ./havlak4 $
现在的速度比开始时快2.11倍。 让我们再看一下CPU profile。
GC CPU Profile
$ make havlak4.prof ./havlak4 -cpuprofile=havlak4.prof # of loops: 76000 (including 1 artificial root node) $ go tool pprof havlak4 havlak4.prof Welcome to pprof! For help, type 'help'. (pprof) top10 Total: 1173 samples 205 17.5% 17.5% 1083 92.3% main.FindLoops 138 11.8% 29.2% 215 18.3% scanblock 88 7.5% 36.7% 96 8.2% sweepspan 76 6.5% 43.2% 597 50.9% runtime.mallocgc 75 6.4% 49.6% 78 6.6% runtime.settype_flush 74 6.3% 55.9% 75 6.4% flushptrbuf 64 5.5% 61.4% 64 5.5% runtime.memmove 63 5.4% 66.8% 524 44.7% runtime.growslice 51 4.3% 71.1% 51 4.3% main.DFS 50 4.3% 75.4% 146 12.4% runtime.MCache_Alloc (pprof)
现在内存分配和随后的垃圾收集(run time.mallocgc
)占我们运行时间的50.9%。
另一种查看系统为什么进行垃圾收集的方法是查看导致收集的分配,即查看在 mallocgc
中花费大部分时间的地方:
(pprof) web mallocgc
很难看清楚图中发生了什么,因为有许多节点的样本数量很少,这些节点模糊了大样本的。我们可以让 go tool pprof
忽略少于10%占比的样本节点:
$ go tool pprof --nodefraction=0.1 havlak4 havlak4.prof Welcome to pprof! For help, type 'help'. (pprof) web mallocgc