mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 23:26:38 +08:00
282 lines
6.2 KiB
Markdown
282 lines
6.2 KiB
Markdown
# 基准测试方法
|
|
|
|
## 编写基准测试
|
|
|
|
Go 基准测试使用 `testing.B` 类型,位于 `_test.go` 文件中。
|
|
基准测试函数名必须以 `Benchmark` 开头。
|
|
|
|
```go
|
|
func BenchmarkStrconv(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
s := strconv.Itoa(rand.Int())
|
|
_ = s
|
|
}
|
|
}
|
|
|
|
func BenchmarkFmtSprint(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
s := fmt.Sprint(rand.Int())
|
|
_ = s
|
|
}
|
|
}
|
|
```
|
|
|
|
关键规则:
|
|
- 使用 `b.N` 作为循环边界——框架会调整它以获得稳定的计时
|
|
- 将结果赋值给变量(或 `_`),防止编译器优化掉调用
|
|
- 在不需要测量的昂贵设置之后使用 `b.ResetTimer()`
|
|
- 使用 `b.ReportAllocs()` 或 `-benchmem` 标志跟踪分配情况
|
|
|
|
### 子基准测试
|
|
|
|
```go
|
|
func BenchmarkConvert(b *testing.B) {
|
|
for _, size := range []int{10, 100, 1000} {
|
|
b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
|
|
data := make([]byte, size)
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = string(data)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 运行基准测试
|
|
|
|
```bash
|
|
# 运行包中的所有基准测试
|
|
go test -bench=. ./...
|
|
|
|
# 运行特定基准测试并显示内存统计
|
|
go test -bench=BenchmarkStrconv -benchmem ./...
|
|
|
|
# 多次运行以获得统计显著性
|
|
go test -bench=. -benchmem -count=10 ./...
|
|
```
|
|
|
|
`-benchmem` 标志报告每次操作的分配次数。`-count` 标志将每个基准测试运行 N 次以获得统计显著性。
|
|
|
|
---
|
|
|
|
## 解读结果
|
|
|
|
```
|
|
BenchmarkStrconv-8 18705042 64.2 ns/op 16 B/op 1 allocs/op
|
|
BenchmarkFmtSprint-8 8249536 143.0 ns/op 16 B/op 2 allocs/op
|
|
```
|
|
|
|
| 字段 | 含义 |
|
|
|------|------|
|
|
| `-8` | GOMAXPROCS |
|
|
| `18705042` | 迭代次数 |
|
|
| `64.2 ns/op` | 每次操作时间 |
|
|
| `16 B/op` | 每次操作分配的字节数 |
|
|
| `1 allocs/op` | 每次操作的堆分配次数 |
|
|
|
|
---
|
|
|
|
## 使用 benchstat 进行比较
|
|
|
|
`benchstat` 对基准测试结果进行统计比较。安装它并将基准测试输出保存到文件:
|
|
|
|
```bash
|
|
# 安装 benchstat
|
|
go install golang.org/x/perf/cmd/benchstat@latest
|
|
|
|
# 运行基准测试并保存结果
|
|
go test -bench=. -benchmem -count=10 ./... > old.txt
|
|
|
|
# 进行修改后再次运行
|
|
go test -bench=. -benchmem -count=10 ./... > new.txt
|
|
|
|
# 比较结果
|
|
benchstat old.txt new.txt
|
|
```
|
|
|
|
### 解读 benchstat 输出
|
|
|
|
```
|
|
name old time/op new time/op delta
|
|
Strconv-8 64.2ns ± 2% 61.8ns ± 1% -3.74% (p=0.001 n=10+10)
|
|
```
|
|
|
|
- **delta**:变化百分比(负数 = 更快)
|
|
- **p-value**:统计显著性(p < 0.05 为显著)
|
|
- **n**:使用的有效样本数量
|
|
|
|
提示:
|
|
- 始终使用 `-count=10` 或更高以获得可靠结果
|
|
- 小的 p 值确认变化是真实的,而非噪声
|
|
- 如果 benchstat 显示 `~`(波浪号),则差异不具有统计显著性
|
|
|
|
---
|
|
|
|
## 来自性能模式的基准测试示例
|
|
|
|
### strconv vs fmt
|
|
|
|
| 方式 | 速度 | 分配次数 |
|
|
|------|------|---------|
|
|
| `fmt.Sprint` | 143 ns/op | 2 allocs/op |
|
|
| `strconv.Itoa` | 64.2 ns/op | 1 allocs/op |
|
|
|
|
### 重复字节转换
|
|
|
|
```go
|
|
func BenchmarkRepeatedConversion(b *testing.B) {
|
|
var buf bytes.Buffer
|
|
for i := 0; i < b.N; i++ {
|
|
buf.Write([]byte("Hello world"))
|
|
}
|
|
}
|
|
|
|
func BenchmarkSingleConversion(b *testing.B) {
|
|
var buf bytes.Buffer
|
|
data := []byte("Hello world")
|
|
for i := 0; i < b.N; i++ {
|
|
buf.Write(data)
|
|
}
|
|
}
|
|
```
|
|
|
|
| 方式 | 速度 |
|
|
|------|------|
|
|
| 重复转换 | 22.2 ns/op |
|
|
| 单次转换 | 3.25 ns/op |
|
|
|
|
### Slice 容量
|
|
|
|
```go
|
|
func BenchmarkNoCapacity(b *testing.B) {
|
|
for n := 0; n < b.N; n++ {
|
|
data := make([]int, 0)
|
|
for k := 0; k < 1000; k++ {
|
|
data = append(data, k)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkWithCapacity(b *testing.B) {
|
|
for n := 0; n < b.N; n++ {
|
|
data := make([]int, 0, 1000)
|
|
for k := 0; k < 1000; k++ {
|
|
data = append(data, k)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
| 方式 | 时间(1 亿次迭代) |
|
|
|------|------------------------|
|
|
| 无容量 | 2.48s |
|
|
| 指定容量 | 0.21s |
|
|
|
|
---
|
|
|
|
## 使用 pprof 进行性能分析
|
|
|
|
使用 `pprof` 在优化前识别瓶颈。基准测试衡量改进效果;pprof 找到需要改进的地方。
|
|
|
|
### CPU 性能分析
|
|
|
|
```bash
|
|
# 从基准测试生成 CPU 分析文件
|
|
go test -bench=BenchmarkHotPath -cpuprofile=cpu.prof ./...
|
|
|
|
# 使用 pprof 分析
|
|
go tool pprof cpu.prof
|
|
```
|
|
|
|
常用 pprof 命令:
|
|
|
|
```
|
|
(pprof) top10 # 按 CPU 时间排列的前 10 个函数
|
|
(pprof) list funcName # 某个函数的带注释源码
|
|
(pprof) web # 浏览器中的交互式图表
|
|
```
|
|
|
|
### 内存性能分析
|
|
|
|
```bash
|
|
# 生成内存分析文件
|
|
go test -bench=BenchmarkHotPath -memprofile=mem.prof ./...
|
|
|
|
# 分析分配情况
|
|
go tool pprof -alloc_space mem.prof
|
|
```
|
|
|
|
### 运行中服务的 HTTP 性能分析
|
|
|
|
```go
|
|
import _ "net/http/pprof"
|
|
|
|
func main() {
|
|
go func() {
|
|
log.Println(http.ListenAndServe("localhost:6060", nil))
|
|
}()
|
|
// ... 应用程序代码 ...
|
|
}
|
|
```
|
|
|
|
通过 `http://localhost:6060/debug/pprof/` 访问性能分析数据。
|
|
|
|
### 性能分析工作流
|
|
|
|
1. 对疑似热点路径进行**基准测试**
|
|
2. 使用 pprof **分析**以确认时间花在了哪里
|
|
3. 使用本技能中的模式进行**优化**
|
|
4. **重新基准测试**以用 benchstat 验证改进
|
|
5. **重新分析**以检查是否出现新的瓶颈
|
|
|
|
---
|
|
|
|
## 常见错误
|
|
|
|
### 忽略 b.N
|
|
|
|
测试框架会调整 `b.N` 以获得稳定的计时。使用固定迭代次数会产生无意义的结果:
|
|
|
|
```go
|
|
// 不好:忽略 b.N —— 基准测试框架无法校准
|
|
func BenchmarkFixed(b *testing.B) {
|
|
for i := 0; i < 1000; i++ {
|
|
doWork()
|
|
}
|
|
}
|
|
|
|
// 好:使用 b.N 作为循环边界
|
|
func BenchmarkCorrect(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
doWork()
|
|
}
|
|
}
|
|
```
|
|
|
|
### 未防止编译器优化消除
|
|
|
|
如果函数调用的结果未被使用,编译器可能会完全优化掉该调用。将结果赋值给包级变量:
|
|
|
|
```go
|
|
// 不好:编译器可能会优化掉调用
|
|
func BenchmarkElided(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
expensiveFunc()
|
|
}
|
|
}
|
|
|
|
// 好:赋值给包级变量以防止优化消除
|
|
var benchResult int
|
|
|
|
func BenchmarkKept(b *testing.B) {
|
|
var r int
|
|
for i := 0; i < b.N; i++ {
|
|
r = expensiveFunc()
|
|
}
|
|
benchResult = r
|
|
}
|
|
```
|