mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-10 17:26:38 +08:00
wavelet init
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
# 基准测试方法
|
||||
|
||||
## 编写基准测试
|
||||
|
||||
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
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,134 @@
|
||||
# 字符串优化模式
|
||||
|
||||
## strconv vs fmt
|
||||
|
||||
在基本类型和字符串之间转换时,`strconv` 比 `fmt` 更快,因为 `fmt` 使用反射并处理任意类型。
|
||||
|
||||
**不好:**
|
||||
|
||||
```go
|
||||
for i := 0; i < b.N; i++ {
|
||||
s := fmt.Sprint(rand.Int())
|
||||
}
|
||||
```
|
||||
|
||||
**好:**
|
||||
|
||||
```go
|
||||
for i := 0; i < b.N; i++ {
|
||||
s := strconv.Itoa(rand.Int())
|
||||
}
|
||||
```
|
||||
|
||||
**基准测试比较:**
|
||||
|
||||
| 方式 | 速度 | 分配次数 |
|
||||
|------|------|---------|
|
||||
| `fmt.Sprint` | 143 ns/op | 2 allocs/op |
|
||||
| `strconv.Itoa` | 64.2 ns/op | 1 allocs/op |
|
||||
|
||||
常用转换:
|
||||
|
||||
| 任务 | `fmt` | `strconv` |
|
||||
|------|-------|-----------|
|
||||
| Int → string | `fmt.Sprint(n)` | `strconv.Itoa(n)` |
|
||||
| Int64 → string | `fmt.Sprint(n)` | `strconv.FormatInt(n, 10)` |
|
||||
| Float → string | `fmt.Sprint(f)` | `strconv.FormatFloat(f, 'f', -1, 64)` |
|
||||
| String → int | — | `strconv.Atoi(s)` |
|
||||
| Bool → string | `fmt.Sprint(b)` | `strconv.FormatBool(b)` |
|
||||
|
||||
---
|
||||
|
||||
## 重复的字符串到字节转换
|
||||
|
||||
不要重复从固定字符串创建字节切片。应该只转换一次并保存结果。
|
||||
|
||||
**不好:**
|
||||
|
||||
```go
|
||||
for i := 0; i < b.N; i++ {
|
||||
w.Write([]byte("Hello world"))
|
||||
}
|
||||
```
|
||||
|
||||
**好:**
|
||||
|
||||
```go
|
||||
data := []byte("Hello world")
|
||||
for i := 0; i < b.N; i++ {
|
||||
w.Write(data)
|
||||
}
|
||||
```
|
||||
|
||||
**基准测试比较:**
|
||||
|
||||
| 方式 | 速度 |
|
||||
|------|------|
|
||||
| 重复转换 | 22.2 ns/op |
|
||||
| 单次转换 | 3.25 ns/op |
|
||||
|
||||
好的版本**快约 7 倍**,因为它避免了每次迭代都分配新的字节切片。
|
||||
|
||||
---
|
||||
|
||||
## 字符串拼接
|
||||
|
||||
根据复杂度选择正确的字符串构建策略。
|
||||
|
||||
### 简单场景使用 `+`
|
||||
|
||||
```go
|
||||
key := "projectid: " + p
|
||||
```
|
||||
|
||||
`+` 运算符对于少量、固定数量的字符串是高效的。编译器通常可以优化相邻的字符串字面量。
|
||||
|
||||
### 格式化使用 `fmt.Sprintf`
|
||||
|
||||
```go
|
||||
// 好:清晰的格式化
|
||||
str := fmt.Sprintf("%s [%s:%d]-> %s", src, qos, mtu, dst)
|
||||
|
||||
// 不好:使用 + 手动转换
|
||||
str := src.String() + " [" + qos.String() + ":" + strconv.Itoa(mtu) + "]-> " + dst.String()
|
||||
```
|
||||
|
||||
当写入 `io.Writer` 时,直接使用 `fmt.Fprintf` 而不是先用 `fmt.Sprintf` 构建临时字符串。
|
||||
|
||||
### 逐段构建使用 `strings.Builder`
|
||||
|
||||
`strings.Builder` 花费摊销线性时间,而重复使用 `+` 或
|
||||
`fmt.Sprintf` 在构建大字符串时花费二次时间:
|
||||
|
||||
```go
|
||||
b := new(strings.Builder)
|
||||
for i, d := range digitsOfPi {
|
||||
fmt.Fprintf(b, "the %d digit of pi is: %d\n", i, d)
|
||||
}
|
||||
str := b.String()
|
||||
```
|
||||
|
||||
### 常量多行字符串使用反引号
|
||||
|
||||
```go
|
||||
// 好:原始字符串字面量
|
||||
usage := `Usage:
|
||||
|
||||
custom_tool [args]`
|
||||
|
||||
// 不好:使用转义序列拼接
|
||||
usage := "" +
|
||||
"Usage:\n" +
|
||||
"\n" +
|
||||
"custom_tool [args]"
|
||||
```
|
||||
|
||||
### 策略总结
|
||||
|
||||
| 方法 | 最佳用途 | 性能 |
|
||||
|------|---------|------|
|
||||
| `+` | 少量字符串,简单拼接 | 小 n 时 O(n) |
|
||||
| `fmt.Sprintf` | 格式化输出 | 较慢,但更清晰 |
|
||||
| `strings.Builder` | 循环/逐段构建 | 摊销 O(n) |
|
||||
| `strings.Join` | 连接 slice | O(n) |
|
||||
| 反引号字面量 | 常量多行文本 | 零开销 |
|
||||
Reference in New Issue
Block a user