Go通常用于编写分布式系统,高级数据存储和微服务。 在这些领域,性能至关重要。
在本教程中,您将学习如何对程序进行概要分析,以使其快如闪电(更好地利用CPU)或轻巧(占用更少的内存)。 我将使用pprof(Go分析器)介绍CPU和内存配置文件,可视化配置文件,甚至显示火焰图。
分析是在各个方面衡量程序的性能。 Go带有强大的剖析支持,可以立即剖析以下尺寸:
- 每个功能AND指令的CPU时间采样
- 所有堆分配的采样
- 当前所有goroutine的堆栈跟踪
- 导致创建新OS线程的堆栈跟踪
- 导致跟踪原语阻塞的堆栈跟踪
- 竞争互斥体的堆栈痕迹
您甚至可以根据需要创建自定义配置文件。 执行概要分析包括创建一个配置文件,然后使用pprof
go工具对其进行分析。
如何创建配置文件
有几种创建配置文件的方法。
使用“开始测试”生成配置文件
最简单的方法是使用go test
。 它具有几个标志,可让您创建配置文件。 以下是在当前目录中为测试生成CPU配置文件和内存配置文件的方法: go test -cpuprofile cpu.prof -memprofile mem.prof -bench .
从长期运行的服务下载实时配置文件数据
如果要分析长期运行的Web服务,则可以使用内置的HTTP接口来提供配置文件数据。 在以下位置添加以下导入语句:
import _ "net/http/pprof"
现在,您可以从/debug/pprof/
URL下载实时配置文件数据。 有关更多信息,请参见net / http / pprof软件包文档。
代码剖析
您也可以将直接配置文件添加到代码中以进行完全控制。 首先,您需要导入runtime/pprof
。 CPU分析由两个调用控制:
-
pprof.StartCPUProfile()
-
pprof.StopCPUProfile()
内存分析是通过调用runtime.GC()
和pprof.WriteHeapProfile()
。
所有性能分析功能都接受一个文件句柄,您负责适当地打开和关闭该文件句柄。
示例程序
为了查看分析器的运行情况,我将使用一个解决Euler项目8的程序 。 问题是:给定一个1,000位数字,找到该数字中乘积最大的13位相邻数字。
这是一个简单的解决方案,它迭代所有13位数字的序列,对于每个这样的序列,它将所有13位数字相乘并返回结果。 最大的结果被存储并最终返回:
package trivial
import (
"strings"
)
func calcProduct(series string) int64 {
digits := make([]int64, len(series))
for i, c := range series {
digits[i] = int64(c) - 48
}
product := int64(1)
for i := 0; i < len(digits); i++ {
product *= digits[i]
}
return product
}
func FindLargestProduct(text string) int64 {
text = strings.Replace(text, "\n", "", -1)
largestProduct := int64(0)
for i := 0; i < len(text); i++ {
end := i + 13
if end > len(text) {
end = len(text)
}
series := text[i:end]
result := calcProduct(series)
if result > largestProduct {
largestProduct = result
}
}
return largestProduct
}
稍后,在进行概要分析之后,我们将看到使用另一种解决方案来提高性能的一些方法。
CPU分析
让我们来分析程序的CPU。 我将使用此测试使用go测试方法:
import (
"testing"
)
const text = `
73167176531330624919225119674426574742355349194934
96983520312774506326239578318016984801869478851843
85861560789112949495459501737958331952853208805511
12540698747158523863050715693290963295227443043557
66896648950445244523161731856403098711121722383113
62229893423380308135336276614282806444486645238749
30358907296290491560440772390713810515859307960866
70172427121883998797908792274921901699720888093776
65727333001053367881220235421809751254540594752243
52584907711670556013604839586446706324415722155397
53697817977846174064955149290862569321978468622482
83972241375657056057490261407972968652414535100474
82166370484403199890008895243450658541227588666881
16427171479924442928230863465674813919123162824586
17866458359124566529476545682848912883142607690042
24219022671055626321111109370544217506941658960408
07198403850962455444362981230987879927244284909188
84580156166097919133875499200524063689912560717606
05886116467109405077541002256983155200055935729725
71636269561882670428252483600823257530420752963450
`
func TestFindLargestProduct(t *testing.T) {
for i := 0; i < 100000; i++ {
res := FindLargestProduct(text)
expected := int64(23514624000)
if res != expected {
t.Errorf("Wrong!")
}
}
}
请注意,我运行了100,000次测试,因为go探查器是一个采样探查器,它需要代码实际上在每行代码上花费一些可观的时间(累计几毫秒)。 这是准备配置文件的命令:
go test -cpuprofile cpu.prof -bench .
ok _/github.com/the-gigi/project-euler/8/go/trivial 13.243s
花了13秒多一点(进行了100,000次迭代)。 现在,要查看配置文件,请使用pprof go工具进入交互式提示。 有许多命令和选项。 最基本的命令是topN; 使用-cum选项,它显示了执行时间最多的前N个函数(因此,执行时间很少,但是被调用很多的函数可以位于顶部)。 这通常是我的开始。
> go tool pprof cpu.prof
Type: cpu
Time: Oct 23, 2017 at 8:05am (PDT)
Duration: 13.22s, Total samples = 13.10s (99.06%)
Entering interactive mode (type "help" for commands)
(pprof) top5 -cum
Showing nodes accounting for 1.23s, 9.39% of 13.10s total
Dropped 76 nodes (cum <= 0.07s)
Showing top 5 nodes out of 53
flat flat% sum% cum cum%
0.07s 0.53% 0.53% 10.64s 81.22% FindLargestProduct
0 0% 0.53% 10.64s 81.22% TestFindLargestProduct
0 0% 0.53% 10.64s 81.22% testing.tRunner
1.07s 8.17% 8.70% 10.54s 80.46% trivial.calcProduct
0.09s 0.69% 9.39% 9.47s 72.29% runtime.makeslice
让我们了解输出。 每行代表一个功能。 由于空间限制,我省略了每个函数的路径,但是它将在实际输出中显示为最后一列。
固定表示在函数中花费的时间(或百分比),而Cum表示累计-在函数及其中调用的所有函数中花费的时间。 在这种情况下, testing.tRunner
实际上调用了TestFindLargestProduct()
,后者调用了FindLargestProduct()
,但是由于实际上没有时间在此花费,因此采样探查器将其固定时间计为0。
内存分析
内存配置文件类似,除了创建内存配置文件之外:
go test -memprofile mem.prof -bench .
PASS
ok _/github.com/the-gigi/project-euler/8/go/trivial
您可以使用同一工具分析内存使用情况。
使用pprof优化程序速度
让我们看看如何才能更快地解决问题。 纵观轮廓,我们看到calcProduct()
取平运行时的8.17%,但makeSlice()
它是由被称为calcProduct()
正以72%(累计因为它调用等功能)。 这很好地表明了我们需要优化的内容。 该代码做什么? 对于13个相邻数字的每个序列,它分配一个切片:
func calcProduct(series string) int64 {
digits := make([]int64, len(series))
...
每次运行几乎是1,000次,而我们运行100,000次。 内存分配很慢。 在这种情况下,实际上不需要每次都分配新的片。 实际上,根本不需要分配任何片。 我们可以只扫描输入数组。
下面的代码片段显示了如何通过简单地除以上一个序列的第一个数字并乘以cur
数字来计算运行乘积。
if cur == 1 {
currProduct /= old
continue
}
if old == 1 {
currProduct *= cur
} else {
currProduct = currProduct / old * cur
}
if currProduct > largestProduct {
largestProduct = currProduct
}
这是一些算法优化的简短列表:
- 计算正在运行的产品。 假设我们在索引N ... N + 13处计算乘积,并将其称为P(N)。 现在我们需要计算索引为N + 1..N + 13的乘积。 P(N + 1)等于P(N),只是索引N的第一个数字消失了,我们需要考虑索引N + 14T的新数字。 可以通过将前一个乘积除以第一个乘积再乘以新乘积来完成。
- 不计算包含0的13个数字的任何序列(乘积始终为零)。
- 避免除以1。
完整的程序在这里。 有一些棘手的逻辑可以解决零,但除此之外,它非常简单。 最主要的是,我们仅在开始时分配一个1000字节的数组,并通过指针(因此没有副本)将其findLargestProductInSeries()
给具有一定范围索引的findLargestProductInSeries()
函数。
package scan
func findLargestProductInSeries(digits *[1000]byte,
start, end int) int64 {
if (end - start) < 13 {
return -1
}
largestProduct := int64((*digits)[start])
for i := 1; i < 13 ; i++ {
d := int64((*digits)[start + i])
if d == 1 {
continue
}
largestProduct *= d
}
currProduct := largestProduct
for ii := start + 13; ii < end; ii++ {
old := int64((*digits)[ii-13])
cur := int64((*digits)[ii])
if old == cur {
continue
}
if cur == 1 {
currProduct /= old
continue
}
if old == 1 {
currProduct *= cur
} else {
currProduct = currProduct / old * cur
}
if currProduct > largestProduct {
largestProduct = currProduct
}
}
return largestProduct
}
func FindLargestProduct(text string) int64 {
var digits [1000]byte
digIndex := 0
for _, c := range text {
if c == 10 {
continue
}
digits[digIndex] = byte(c) - 48
digIndex++
}
start := -1
end := -1
findStart := true
var largestProduct int64
for ii := 0; ii < len(digits) - 13; ii++ {
if findStart {
if digits[ii] == 0 {
continue
} else {
start = ii
findStart = false
}
}
if digits[ii] == 0 {
end = ii
result := findLargestProductInSeries(&digits,
start,
end)
if result > largestProduct {
largestProduct = result
}
findStart = true
}
}
return largestProduct
}
测试是一样的。 让我们看看如何处理配置文件:
> go test -cpuprofile cpu.prof -bench .
PASS
ok _/github.com/the-gigi/project-euler/8/go/scan 0.816s
马上,我们可以看到运行时间从超过13秒减少到不到1秒。 很好 是时候窥视一下了。 让我们只使用top10
,它按固定时间排序。
(pprof) top10
Showing nodes accounting for 560ms, 100% of 560ms total
flat flat% sum% cum cum%
290ms 51.79% 51.79% 290ms 51.79% findLargestProductInSeries
250ms 44.64% 96.43% 540ms 96.43% FindLargestProduct
20ms 3.57% 100% 20ms 3.57% runtime.usleep
0 0% 100% 540ms 96.43% TestFindLargestProduct
0 0% 100% 20ms 3.57% runtime.mstart
0 0% 100% 20ms 3.57% runtime.mstart1
0 0% 100% 20ms 3.57% runtime.sysmon
0 0% 100% 540ms 96.43% testing.tRunner
这很棒。 整个运行时间几乎都花在了我们的代码中。 根本没有内存分配。 我们可以更深入地研究,并使用list命令查看语句级别:
(pprof) list FindLargestProduct
Total: 560ms
ROUTINE ======================== scan.FindLargestProduct
250ms 540ms (flat, cum) 96.43% of Total
. . 44:
. . 45:
. . 46:func FindLargestProduct(t string) int64 {
. . 47: var digits [1000]byte
. . 48: digIndex := 0
70ms 70ms 49: for _, c := range text {
. . 50: if c == 10 {
. . 51: continue
. . 52: }
. . 53: digits[digIndex] = byte(c) - 48
10ms 10ms 54: digIndex++
. . 55: }
. . 56:
. . 57: start := -1
. . 58: end := -1
. . 59: findStart := true
. . 60: var largestProduct int64
. . 61: for ii := 0; ii < len(digits)-13; ii++ {
10ms 10ms 62: if findStart {
. . 63: if digits[ii] == 0 {
. . 64: continue
. . 65: } else {
. . 66: start = ii
. . 67: findStart = false
. . 68: }
. . 69: }
. . 70:
70ms 70ms 71: if digits[ii] == 0 {
. . 72: end = ii
20ms 310ms 73: result := f(&digits,start,end)
70ms 70ms 74: if result > largestProduct {
. . 75: largestProduct = result
. . 76: }
. . 77: findStart = true
. . 78: }
. . 79: }
这真是太神奇了。 您可以通过陈述时机掌握所有要点。 请注意,第73行对function f()
的调用实际上是对findLargestProductInSeries()
的调用,由于空间限制,我在配置文件findLargestProductInSeries()
其重命名。 通话时间为20毫秒。 也许通过将功能代码嵌入到位,我们可以保存功能调用(包括分配堆栈和复制参数)并保存这20毫秒。 该视图还可以帮助您确定其他有价值的优化。
可视化
对于大型程序,查看这些文本配置文件可能很困难。 Go提供了许多可视化选项。 下一节需要安装Graphviz 。
pprof工具可以生成多种格式的输出。 最简单的方法之一(svg输出)是从pprof交互式提示中键入“ web”,您的浏览器将显示一个漂亮的图形,并用粉红色标记热路径。
火焰图
内置图很好,而且很有帮助,但是对于大型程序而言,即使是这些图也可能难以探索。 用于可视化性能结果的最受欢迎的工具之一是火焰图 。 pprof工具尚不支持它,但是您可以使用Uber的go-torch工具来处理火焰图。 正在为pprof添加对火焰图的内置支持。
结论
Go是一种系统编程语言,用于构建高性能的分布式系统和数据存储。 Go附带了出色的支持,可以更好地对程序进行性能分析,分析其性能并可视化结果。
Go团队和社区非常重视改进围绕性能的工具。 可以在GitHub上找到具有三种不同算法的完整源代码。
翻译自: https://code.tutsplus.com/tutorials/make-your-go-programs-lightning-fast-with-profiling--cms-29809