diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..2c4fb9326bd871edcbc6dd93f1127e00a5622209 Binary files /dev/null and b/.DS_Store differ diff --git a/chap-34-benchmarks/.DS_Store b/chap-34-benchmarks/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..6ea29bd170f15b8a97f08e55a9f7f706b49efd49 Binary files /dev/null and b/chap-34-benchmarks/.DS_Store differ diff --git a/chap-34-benchmarks/imgs/benchmark_results.d02152ba.png b/chap-34-benchmarks/imgs/benchmark_results.d02152ba.png new file mode 100644 index 0000000000000000000000000000000000000000..74c29843761a5f172749df72aa8ad1f3749dc7af Binary files /dev/null and b/chap-34-benchmarks/imgs/benchmark_results.d02152ba.png differ diff --git "a/chap-34-benchmarks/\347\254\25434\347\253\240\357\274\232benchmarks\357\274\210\345\237\272\345\207\206\346\265\213\350\257\225\357\274\211.md" "b/chap-34-benchmarks/\347\254\25434\347\253\240\357\274\232benchmarks\357\274\210\345\237\272\345\207\206\346\265\213\350\257\225\357\274\211.md" new file mode 100644 index 0000000000000000000000000000000000000000..9817edbd371303dedba6edfb828780385b2c9e13 --- /dev/null +++ "b/chap-34-benchmarks/\347\254\25434\347\253\240\357\274\232benchmarks\357\274\210\345\237\272\345\207\206\346\265\213\350\257\225\357\274\211.md" @@ -0,0 +1,652 @@ +# 第34章:benchmarks(基准测试) + +## 一、你将会在这一章学到什么? + +* 什么是benchmark? +* 怎么写一个benchmark。 +* 怎么阅读benchmark的结果。 + +## 二、技术概念涵盖 + +* benchmark(基准测试) +* solver(解决方案) +* 内存分配(动态的和静态的) + +## 三、介绍 + +一个问题可能会有不同的解决方案。 + +让我们举个例子:你不小心弄丢了你的钥匙,但是你想打开自己家的门。这个问题有几个解决方案。你可以: + +1. 打电话给有共享钥匙的同伴 + +2. 打电话给锁匠来开门 + +3. 返回你弄丢钥匙的地方寻找钥匙。要是在三个小时之内没有找到,采取上面两种方案。 + +4. 打碎玻璃进入房子 + +这些解决方案都有同一个结果;你会打开房门。但是如果你稍微思考一下,您能够给这些方案按照花费时间排序。方案2和4将会花费你的金钱。方案3很有可能会花费你更多的时间。但是如果你只是将钥匙忘在了自己的车上,而停车场离你你只有五分钟路程远。显然在这种情况下,方案3将会比预期花费的少。 + +通过在脑海里测试不同的可能性方案,你就是在进行benchmark(基准测试) + +## 四、什么是基准测试 + +基准测试是用来比较系统和组件的工具。 + +设计和运行基准测试的目的是找到最佳的解决策略。(也叫做solver) + +解决方案(solver)常常是一个方法。 + +为了选择最佳的solver,需要制定一个规则。在基准测试中,程序执行的统计数字将会被收集起来(计算时间、affectation是什么?、函数调用次数等)。有了这些统计数字,我们能选择一个决策规则。 + +这里并没有一个通用规则。规则可能会随着你的需求变化,比如,如果你想选择CPU占用更少的程序,你只用关心这些统计数字即可。如果你设计的程序运行在只有很少存储空间可用的设备上,你也许会更关注内存使用的统计数字来选择最佳方案(solver)。 + +## 五、怎么写一个基准测试 + +我们将比较两个将字符串连接起来的算法。第一步是创建两个函数分别使用这两种方案: + +```go +//benchmark/basic/bench.go +package basic + +import( + "bytes" + "strings" +) + +func ConcatenateBuffer(first string,second string)string{ + var buffer bytes.Buffer + buffer.WriteString(first) + buffer.WriteString(second) + return buffer.String() +} + +func ConcatenateJoin(first string,second string)string{ + return strings.Join([]string{first,second},"") +} +``` + +连两个函数都实现了将两个字符串连接的功能。他们分别使用了不同的方法。第一个函数`ConcatenateBuffer`使用一个buffer(来自`buffer`包)。第二个函数将来自`strings`包的`Join`函数封装了一下。我们想知道这两种方式哪一种最好。 + +benchmarks are living next to the unit tests。一个基准测试是在一个测试文件里的函数。它的名字必须以Benchmark开头。基准测试函数有着以下的结构: + +```go +func BenchmarkXXX(b *testing.B){ + +} +``` + +这个函数有一个`testing.B`类型的指针。这个类型结构只有一个可导出的属性:`N`,表示需要跑的测试的数量(这里iteration还不确定怎么翻译)基准测试会对同一个函数跑好几次测试来收集可靠的数据。这是为什么基准测试函数总是含括这种循环: + +```go +for i:=0;i>trace.log +``` + +GODEBUG是一个环境变量,接受键值对列表。在这里我们告诉go的运行时为每次内存分配和释放生成堆栈追踪。接着使用`&>>trace.log`将标准输出和标准错误输出重定向到文件trace.log。如果存在该文件,将会把日志信息追加到文件。如果没有,创建该文件。 + +在trace.log中,记录了1034行文本,包含了堆栈追踪信息。怎么利用该信息?如果我们引用该文档,程序的每次内存分配都会生成一次堆栈追踪信息。 + +可以使用cat和grep命令来寻找想要的信息: + +```shell +$ cat trace.log | grep -n /path/to/the/package/basic/bench.go +``` + +使用cat打印文件中所有信息,grep正则匹配想要的信息,这里文件的路径需要使用你自己的路径。 + +| 管道命令用来链接命令。第一个命令的输出将会作为第二个命令的输入。整个命令构成一个管道。 + +接下来是我们的输出: + +```go +988: /path/to/the/package/basic/bench.go:9 +0x31 fp=0xc000044758 sp=0xc000044710 pc=0x1055c81 +1005: /path/to/the/package/basic/bench.go:12 +0xca fp=0xc000044758 sp=0xc000044710 pc=0x1055d1a +1028: /path/to/the/package/basic/bench.go:16 +0x7e fp=0xc00008af58 sp=0xc00008aef0 pc=0x1055dde +``` + +添加参数-n 返回匹配到的字符串那一行的内容。我们在文件中发现了三次内存分配。在路径的旁边,是造成内存分配的代码位于的行数。 + +接下来是分析你的代码,看看哪里做了内存分配,以及如何避免。在ConcatenateBuffer函数中,以下两行造成了内存分配: + +```go +var buffer bytes.Buffer +``` + +创建变量buffer + +```go +buffer.String() +``` + +调用String方法 + +调试选项的完整清单在这里: + +https://golang.org/pkg/runtime/#hdr-Environment_Variables + +## 十、输入变量的基准测试 + +在前面的小节中,我们设计的基准测试的参数都是一致的。这种方法足以用于大多数情况。但是你可能会需要了解参数变化时函数的行为。 + +我们使用`testing`包中定义的方法`Run`。这个方法的接收器是`testing.B`类型的指针 + +如果我们想深入分析,可以使用变长字符串测试我们的两个函数。我们使用2的幂的长度序列: + +* 2 +* 16 +* 128 +* 1024 +* 8192 +* 65536 +* 524288 +* 4194304 +* 16777216 +* 134217728 + +第一步是将这些整数放入名为lengths的切片中: + +```go +lengths := []int{2,16,128,1024,8192,65536,524288,4194304,16777216,134217728} +``` + +使用for循环迭代这些数字。在每次迭代中创建两个不同长度的字符串: + +```go +for _,l :=range lengths{ + first := generateRandomString(l) + second := generateRandomString(l) +} +``` + +一旦生成了这两个字符串,就可以将它们作为参数传入基准测试函数。 + +我们将会创建两个子基准测试。子基准测试使用Run方法声明。它们必须使用典型的基准测试函数声明。我们将封装好的函数命名为`"BenchmarkConcatenation"` + +```go +// benchmark/variable-input/bench_test.go +func BenchmarkConcatenation(b * testing.B){ + var s string + lengths:=[]int{2,16,128,1024,8192,65536,524288,4194304,16777216,134217728} + for _,l:=range lengths{ + first := generateRandomString(l) + second := generateRandomString(l) + } +} +``` + +在循环里面,我们可以调用`b.Run`方法两次(`b.Run`将会创建子基准测试)。首先,我们对`ConcaenateJoin`函数做基准测试: + +```go +b.Run(fmt.Sprintf("ConcatenateJoin-%d",l),func(b *testing.B){ + for i := 0;i> benchmarkConcatenation.log +``` + +随着字符串长度(log-lin plot)的变化,不同方法的执行时间(ns/op)。 + +##### 10.0.0.1 对数尺度 + +上面的图以log-lin plot展示了数据。log-lin plot是一种水平坐标和纵坐标都是对数尺度的图。你可能对这种形式的图不熟悉,如果熟悉的话,可以跳过这一节。 + +对数被用来记录数据范围大的情况。比如我们的随机长度。 + +在我们的情况。推荐使用对数刻度而不是线性刻度。 + +##### 10.0.0.2 解析基准函数结果 + +Go没有内置的工具生成上面的这种图。必须手动解析标准输出以拿到数据。下面是我使用的脚本: + +```go +package main + +import( + "fmt" + "io/ioutil" + "regexp" +) + +func main(){ + b,err := ioutil.ReadFile("/path/to/benchmarkConcatenation.log") + if err != nil{ + panic(err) + } + benchmarkResult := string(b) + regexBench := regexp.MustCompile(`([a-zA-Z]*)-(\d+)-.* (\d+\.?\d+?)[\t]ns.*[\t](\d+)[\t]B.* (\d+) allocs`) + matches := regexBench.FindAllStringSubmatch(benchmarkResult,-1) + fmt.Println("benchmarkedFunction,stringLen,nsPerOp,bytesPerOp,mallocs") + for _,m := range matches{ + fmt.Printf("%s,%s,%s,%s,%s\n",m[1],m[2],m[3],m[4],m[5]) + } +} +``` + +我使用以下有着五个捕获组的正则表达式来获取基准测试数据: + +`([a-zA-Z]*)-(\d+)-.* (\d+\.?\d+?)[\t]ns.*[\t](\d+)[\t]B.* (\d+) allocs` + +在下面的图中,你可以看到对应的捕获组以高亮显示: + +* 第一组捕获基准测试函数名(存放在m[1]) +* 第二组捕获字符串的长度(存放在m[2]) +* 第三组捕获每次操作耗费的时间(单位ns) +* 第四组捕获每次操作分配的字节 +* 第五组捕获每次操作内存分配的次数 + +变量matches是二维字符串切片:`[][]string`。`matches[0]`表示第一次基准测试,`matches[0][1]`表示该次基准测试的名称。 + +##### 10.0.0.3 一点建议 + +* 关注时间变化(ns/op)和内存使用 +* 为你的目标选择有条理的变量 +* 在合适的情况下使用对数刻度的图 + +## 十一、常见错误:b.N作为参数 + +基准测试函数的耗时不应该随着b.N的增加而增加。你的函数输入不应该依赖b.N的数值。否则,你的基准测试结果将不会显著。 + +比如: + +```go +func BenchmarkConcatenateBuffer(b *testing.B){ + var s string + for i:=0;i