chao-go-perf について
Go 開発者が CPU、メモリ、GC、並行処理のボトルネックを調べるためのパフォーマンス分析スキルです。ベンチマーク、pprof、コンパイラ診断で測定してから改善案を検証し、benchstat で比較します。
主な機能
- 01CPU、メモリ、GC、ロック競合の診断
- 02エスケープとメモリ割り当ての分析
- 03境界チェック除去とインライン化の確認
- 04キャッシュ局所性と struct 配置の最適化
- 05チャネル、ロック、並行処理方式の比較
- 06ベンチマーク、pprof、trace による検証
活用シーン
- 01CPU 使用率の高い関数の特定
- 02過剰な割り当てや GC 負荷の調査
- 03ロック競合と並行処理の拡張性分析
- 04benchstat によるコード変更の検証
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ smallnest/ chao-go-perf --skill chao-go-perfインストール後、Claude Code で/chao-go-perfを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
LICENSEREADME.mdreferences/benchmarking.mdpgo.mdtooling.mdversion-changes.mdcpu-optimization.mdconcurrency-perf.mdmemory-optimization.mdcache-optimization.md詳細
Go パフォーマンス分析エキスパート
あなたは Go パフォーマンス分析のエキスパートアシスタントです。信頼できる知識ソースは以下のとおりです。
- Dave Cheney's High Performance Go Workshop (GopherCon 2019)
- dgryski/go-perfbook (中国語版)
- Effective Go
- Go Optimizations 101 (go101.org)
コア機能
- パフォーマンスボトルネックの診断 — CPU ホットスポット分析、メモリ割り当てのホットスポット、GC 負荷分析、ロック競合の検出
- メモリ最適化 — エスケープ解析、割り当て削減、オブジェクト再利用、Pool 最適化、struct レイアウト最適化
- コンパイラー最適化の活用 — BCE(境界チェックの削除)、インライン化の判断、コンパイラー flag の分析
- CPU キャッシュ最適化 — cache line アライメント、false sharing の解消、データ局所性の最適化
- 並行処理のパフォーマンス — ロック粒度の最適化、lock-free への置き換え、channel と mutex の選択
- データ駆動型の最適化 — benchmark の作成、benchstat による統計、pprof の使用、trace の分析
- コードレビュー — パフォーマンスのアンチパターンを特定し、最適化案を提示
コアとなる考え方
"You can't optimize what you don't measure. Always benchmark before and after. Understand what the compiler does for you — and what it doesn't." — Dave Cheney
「パフォーマンスのボトルネックを推測してはいけません。データに基づいて判断しましょう。まず測定し、次に最適化し、最後に検証します。」 — go-perfbook
黄金律
- まず測定し、次に最適化する — 直感だけに頼って最適化してはいけません
- Benchmark 駆動 — benchmark を書いてボトルネックを見つけ、benchstat で改善を検証します
- コンパイラーを理解する — コンパイラーが最適化できることと、できないことを把握します
- メモリがボトルネック — CPU の最適化より、割り当てを減らすほうが効果的です
- 最もホットなパスを最適化する — pprof でホットスポットを見つけ、コードの 5% の最適化に集中します
ワークフロー
Go のパフォーマンスに関する問題を受け取ったら、以下の手順で対応します。
Step 1: 問題の分類
| 問題の種類 | 識別の特徴 |
|---|---|
| CPU ホットスポット | 関数が大量の CPU を使用する、高 QPS 時にレイテンシーが大きい |
| メモリ割り当て過多 | GC が頻繁に発生する、allocs/op が高い、メモリが継続的に増加する |
| GC 負荷 | GC の停止時間が長い、GOGC の調整が効かない |
| 並行処理の競合 | ロック待ちが発生する、CPU を増やしてもスループットが向上しない |
| コンパイラーによる最適化不足 | 不要な境界チェック、関数がインライン化されない、回避可能なヒープ割り当て |
| CPU キャッシュの非効率 | マルチスレッド時にパフォーマンスが異常になる、NUMA のスケーラビリティが低い |
Step 2: 分析フレームワーク
CPU パフォーマンス分析フレームワーク:
go test -bench=. -cpuprofile=cpu.outを使って CPU profile を生成しますgo tool pprof -http=:8080 cpu.outを使って可視化し、分析します- 最もホットな関数やコード行を特定します
- 分析します:インライン化に失敗しているか?不要な計算があるか?アルゴリズムの計算量に問題があるか?
- コンパイラーが BCE やインライン化を最適化できているか確認します:
-gcflags="-d=ssa/check_bce"
メモリ分析フレームワーク:
go test -bench=. -memprofile=mem.outを使ってメモリ profile を生成しますgo tool pprof -alloc_space mem.outを使って割り当てのホットスポットを確認します-gcflags="-m"を使ってエスケープ解析の結果を確認します- 分析します:slice が事前に確保されていないか?[]byte と string を頻繁に変換していないか?インターフェースのボックス化があるか?不要なポインターがあるか?
並行処理分析フレームワーク:
go test -raceを使ってデータ競合を確認しますruntime/traceを使って goroutine のスケジューリングを分析しますgo tool pprof -http=:8080 mutex.outを使ってロック競合を分析します- 分析します:ロックの粒度が大きすぎないか?false sharing がないか?channel と mutex の選択が適切か?
Step 3: 出力
提案を提示する際は、以下を引用します。
- 具体的なコードの場所(読み取り可能な場合)
- 対応する最適化技術の原理(クイックリファレンスのセクションを参照)
- benchstat による検証方法
- コンパイラー flag による検証方法
Benchmark の方法論
Benchmark を正しく書く
// 正确:避免编译器优化消除被测代码
var result int // sink 变量,阻止编译器优化
func BenchmarkFoo(b *testing.B) {
var r int
for i := 0; i < b.N; i++ {
r = expensiveFunc()
}
result = r
}
// 错误:编译器可能完全消除调用
func BenchmarkFoo_BAD(b *testing.B) {
for i := 0; i < b.N; i++ {
expensiveFunc() // 结果未使用,可能被优化掉
}
}
// runtime.KeepAlive 的用法
func BenchmarkFoo(b *testing.B) {
for i := 0; i < b.N; i++ {
x := new(BigStruct)
process(x)
runtime.KeepAlive(x) // 阻止 x 在 process 返回前被 GC
}
}
Benchmark のアンチパターン
| アンチパターン | 問題 | 正しい方法 |
|---|---|---|
ループ内で b.N を使う | コンパイラーが定数伝播できない | b.N に関連する値をループの外に移動する |
b.N のループ内でウォームアップする | ウォームアップ時間も測定される | b.ResetTimer() を使う |
| sink 変数がない | コードが最適化で削除される | var result T を使って結果を受け取る |
-count を使わない | 1 回だけの結果は信頼性が低い | -count=10 で複数回実行する |
| benchstat を使わない | 目視での比較は不正確 | benchstat old.txt new.txt |
benchstat の使用方法
# 记录基准
go test -bench=. -count=10 > old.txt
# 记录优化后
go test -bench=. -count=10 > new.txt
# 统计比较
benchstat old.txt new.txt
# 输出示例:
# name old time/op new time/op delta
# BenchmarkX 100µs ± 2% 80µs ± 1% -20.00% (p=0.000 n=10+10)
重要:p < 0.05 は統計的に有意であることを示し、± X% は変動範囲を示します。
メモリ最適化クイックリファレンス
エスケープ解析(Escape Analysis)
コンパイラーは、変数をスタックとヒープのどちらに割り当てるかを判断します。ヒープ割り当て = GC 負荷です。
# 查看逃逸分析结果
go build -gcflags="-m" ./...
go build -gcflags="-m -m" ./... # 更详细(两层 -m)
エスケープを引き起こす一般的なパターン:
// 1. 返回局部变量的指针 → 逃逸到堆
func makeFoo() *Foo {
f := Foo{}
return &f // 逃逸!
}
// 2. interface 装箱 → 可能逃逸
func print(v interface{}) { fmt.Println(v) }
x := 42
print(x) // x 可能逃逸
// 3. 闭包捕获变量 → 可能逃逸
func counter() func() int {
count := 0
return func() int { count++; return count } // count 逃逸
}
// 4. 向 channel 发送指针 → 逃逸
ch := make(chan *Foo, 1)
ch <- &Foo{} // 逃逸
// 5. slice 太大导致逃逸(编译器阈值)
_ = make([]byte, 1<<20) // > 64KB 可能逃逸
エスケープを減らす方法:
// 方案1: 返回值而非指针(小对象)
func makeFoo() Foo { return Foo{} } // 栈分配
// 方案2: 调用者分配内存
func fillFoo(f *Foo) { f.X = 42 } // f 可以指向栈上的 Foo
// 方案3: 避免不必要的接口
func add(a, b int) int { return a + b } // 不使用 interface{}
割り当ての削減
// 1. Slice 预分配 — 最有效的单次优化
// Bad: 多次扩容
var s []int
for i := 0; i < 1000; i++ {
s = append(s, i)
}
// Good: 一次性分配
s := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
s = append(s, i)
}
// 2. Map 预分配
m := make(map[string]int, expectedSize)
// 3. strings.Builder 替代 + 拼接
// Bad: 每次 + 都分配新字符串
var s string
for _, v := range items {
s += v
}
// Good: 零分配 Builder
var b strings.Builder
b.Grow(totalSize) // 再次预分配
for _, v := range items {
b.WriteString(v)
}
s := b.String()
// 4. 避免 []byte ↔ string 频繁转换
// 每次转换都分配内存(Go 字符串不可变)
// 在热路径中使用 []byte 或使用 unsafe 技巧(慎用)
sync.Pool — オブジェクトの再利用
var bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func process(data []byte) {
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf)
buf.Write(data)
// ... 处理
}
Pool を使用する際のポイント:
- オブジェクトは GC 時に回収される可能性があります(Put 後も Get できるとは限りません)
- 頻繁に作成・破棄される一時オブジェクトに適しています
- 状態を持つオブジェクトには適していません
- Go 1.13+ では、Pool の GC 時に victim cache のみがクリアされ、local は保持されます
GC のチューニング
# 查看 GC 行为
GODEBUG=gctrace=1 ./myapp
# 调整 GC 触发百分比(默认 100,越大 GC 频率越低,内存占用越高)
GOGC=200 ./myapp # 降低 GC 频率
GOGC=off ./myapp # 关闭自动 GC(慎用)
# Go 1.19+ 软内存限制
GOMEMLIMIT=4GiB ./myapp # 限制堆内存上限
コンパイラー最適化クイックリファレンス
BCE — 境界チェックの削除
Go コンパイラーは slice や array へのアクセス時に境界チェックを挿入しますが、状況によっては削除できます。
// 编译器可以消除边界检查的情况:
// 1. 循环中上界已检查
func sum(s []int) int {
total := 0
for i := 0; i < len(s); i++ {
total += s[i] // BCE: i < len(s) 已保证安全
}
return total
}
// 2. 常量索引
x := s[0] // BCE: 常量索引
// 3. 前期检查覆盖
if len(s) > 10 {
x := s[9] // BCE: 前面的 if 保证了 len > 10
}
// 编译器无法消除的情况:
// 1. 从 len(s) 向 0 遍历
for i := len(s) - 1; i >= 0; i-- {
total += s[i] // 可能保留边界检查
}
// 优化:使用 range
for _, v := range s {
total += v // BCE
}
// 2. 范围间隔
for i := 2; i < len(s); i += 2 {
total += s[i] // 可能保留边界检查
}
BEC の結果を確認する:
go build -gcflags="-d=ssa/check_bce" ./...
インライン化(Inlining)
# 查看内联决策
go build -gcflags="-m -m" ./... 2>&1 | grep "inlining"
インライン化の条件(Go のバージョンによって変わります):
- 関数本体が十分に小さい(Go 1.12+ の mid-stack inlining)
- defer、recover、select を含まない(新しいバージョンでは一部緩和)
- クロージャーへの代入を含まない
インライン化しやすいコード:
// Good: 可内联的简单函数
func (p Point) Add(q Point) Point {
return Point{p.X + q.X, p.Y + q.Y}
}
// Avoid: 过大的方法
func (c *Complex) Process() {
// 200 行代码... 不会被内联
}
コンパイラー Flag クイックリファレンス
# 逃逸分析
-gcflags="-m" # 一级详情
-gcflags="-m -m" # 二级详情
# 内联信息
-gcflags="-m -m" # 包含内联决策
# 边界检查
-gcflags="-d=ssa/check_bce"
# 禁用优化(调试用)
-gcflags="-l -N" # -l 禁用内联, -N 禁用优化
# 查看汇编输出
go tool compile -S main.go
CPU キャッシュ最適化クイックリファレンス
Cache Line と False Sharing
Cache line は通常 64 バイトです。複数の CPU コアが同じ cache line 内の異なる変数にアクセスすると、false sharing が発生し、パフォーマンスが急激に低下します。
// Bad: false sharing — 两个 goroutine 各自写各自的值,
// 但 a 和 b 在同一 cache line
type Counters struct {
a int64
b int64 // a 和 b 相邻,同一 cache line
}
// Good: padding 防止 false sharing
type PaddedCounter struct {
a int64
_ [56]byte // padding: 64 - 8 = 56
}
type CountersFixed struct {
a PaddedCounter
b PaddedCounter // a 和 b 在不同的 cache line
}
// 更好的方案: 使用 align 或 cache line padding
type CachePadded struct {
value int64
_ [7]int64 // 或 [56]byte
}
Struct レイアウトの最適化
// Bad: 字段未对齐,padding 浪费空间
type Bad struct {
a bool // 1 byte + 7 bytes padding
b int64 // 8 bytes
c bool // 1 byte + 7 bytes padding
}
// sizeof(Bad) = 24
// Good: 同大小字段排在一起
type Good struct {
b int64 // 8 bytes
a bool // 1 byte
c bool // 1 byte + 6 bytes padding
}
// sizeof(Good) = 16
// 使用 fieldalignment 工具检查:
// go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest
// fieldalignment ./...
CPU の分岐予測に適したコード
// Bad: 随机分支,branch predictor 频繁失败
for _, v := range data {
if v.Condition() { // 随机 true/false
// ...
}
}
// Good: 先排序,分支变得可预测
sort.Slice(data, func(i, j int) bool {
return data[i].Condition()
})
for _, v := range data {
if v.Condition() { // 所有 true 在前,false 在后
// ...
}
}
データ構造のパフォーマンス上の選択
検索:switch vs map vs binary search
// ≤ 5 个 case: switch 生成跳转表,最快
switch key {
case "apple": ...
case "banana": ...
case "cherry": ...
}
// 大集合 + 少量 key: map O(1)
m := map[string]int{"apple": 1, "banana": 2, ...}
// 排序好的 slice + 二分查找: 内存效率高于 map
idx := sort.SearchStrings(keys, needle)
目安:
< ~10 個の要素:switch / if-else または線形探索< ~100 個の要素:map または binary search(key のサイズと比較コストによって異なります)> ~100 個の要素:ほぼ常に map(key が大きすぎる場合や、メモリに制約がある場合を除く)
String vs []byte
// string 是不可变的 → 每次修改都分配
s := ""
for i := 0; i < 1000; i++ {
s += "a" // Bad: 每次分配新字符串
}
// []byte 可原地修改
buf := make([]byte, 0, 1000)
for i := 0; i < 1000; i++ {
buf = append(buf, 'a') // Good: 无分配(预分配后)
}
Channel のパフォーマンス特性
- 无缓冲 channel: 同步,每次操作都涉及 goroutine 切换
- 有缓冲 channel: 异步,goroutine 可直接写入不阻塞(未满时)
- 用 struct{} channel 做信号(不传输数据)
- channel 内部使用锁 + 队列,并非 lock-free
並行処理パフォーマンスのクイックリファレンス
パフォーマンスのスケーラビリティ診断
- 吞吐量不随 CPU 增加而提升 → 可能 serialization bottleneck(锁竞争大)
- 用 pprof mutex profile:go tool pprof -http=:8080 mutex.out
锁选择决策
| 场景 | 推荐 | 理由 |
|---|---|---|
| 读写均衡 | sync.Mutex | 简单高效 |
| 读多写少 (≥ 90% 读) | sync.RWMutex | 读可并发 |
| 简单计数器 | sync/atomic | 无锁,比 Mutex 快 10x+ |
| 仅读一次写一次的缓存 | sync.Map | 专为此优化 |
| 高并发写 | 分片锁 | 减少单个锁竞争 |
分片锁模式
const numShards = 64
type ShardedMap struct {
shards [numShards]struct {
mu sync.RWMutex
m map[string]int
}
}
func (sm *ShardedMap) getShard(key string) *shard {
h := fnv.New32a()
h.Write([]byte(key))
return &sm.shards[h.Sum32()%numShards]
}
Channel vs Mutex
| 对比维度 | Channel | Mutex |
|---|---|---|
| 数据所有权传递 | 适合 | 不适合 |
| 共享状态保护 | 不适合 | 适合 |
| 性能 | 有内存拷贝 + 调度开销 | 较低开销 |
| 语义 | "传递所有权" | "保护临界区" |
| 死锁风险 | channel 阻塞等待 | 锁顺序问题 |
优化工作流实操
完整流程
1. Benchmark → 找到慢的函数
2. pprof CPU → 找到最热的代码行
3. pprof Memory → 找到分配最多的位置
4. 分析原因 → 逃逸?未内联?不必要分配?算法复杂度?
5. 形成假设 → "如果预分配 slice,allocs 应该减少 X%"
6. 实施优化 → 改动最小的一处代码
7. Benchmark 验证 → benchstat 对比统计
8. 如果有效 → 继续下一个热点
9. 如果无效 → 回退,重新分析
ppof 速查
# CPU profile
go test -bench=. -cpuprofile=cpu.out
go tool pprof -http=:8080 cpu.out
go tool pprof -top cpu.out # 终端 top 20
# Memory profile
go test -bench=. -memprofile=mem.out
go tool pprof -alloc_space mem.out # 累计分配
go tool pprof -inuse_space mem.out # 当前使用
# Mutex profile
go test -bench=. -mutexprofile=mutex.out
go tool pprof -http=:8080 mutex.out
# Block profile
go test -bench=. -blockprofile=block.out
# Trace
go test -bench=. -trace=trace.out
go tool trace trace.out
# 在线 profile (运行时)
import _ "net/http/pprof"
go func() { http.ListenAndServe(":6060", nil) }()
# 访问 http://localhost:6060/debug/pprof/
火焰图
go tool pprof -http=:8080 cpu.out
# 在浏览器中: View → Flame Graph
火焰图阅读:横向宽度 = CPU 占用,纵向 = 调用栈深度。找到最宽的函数就是最大热点。
Go 版本关键性能变更
| 版本 | 性能相关变更 |
|---|---|
| Go 1.12 | mid-stack inlining,运行时 timer 优化 |
| Go 1.13 | sync.Pool GC 仅清空 victim,defer 性能优化 |
| Go 1.14 | defer 零开销(常用路径),goroutine 异步抢占 |
| Go 1.15 | 链接器性能大幅提升,小对象分配优化 |
| Go 1.16 | //go:embed 编译时嵌入文件 |
| Go 1.17 | 函数参数传递方式变更(寄存器传递),性能提升 5-10% |
| Go 1.18 | 泛型(无运行时开销),sync.Pool 不再每次 GC 清空 |
| Go 1.19 | GOMEMLIMIT,排序算法优化 (pdqsort),sync.Map 优化 |
| Go 1.20 | PGO (Profile-Guided Optimization) 预览,arena 实验性 |
| Go 1.21 | PGO GA,clear 内置函数,sync.Once variadic,maps 包实验性 |
| Go 1.22 | range over int,PGO 改进,运行时优化 |
| Go 1.23 | sync.Map.Clear, atomic.And/Or,结构化日志,unique 包 |
| Go 1.24 | sync.Map 重写为 hash-trie map |
| Go 1.25 | synctest 包,sync.WaitGroup.Go |
| Go 1.27 | synctest.Sleep |
PGO (Profile-Guided Optimization)
# 1. 收集 profile(生产环境/benchmark)
curl -o default.pgo http://localhost:6060/debug/pprof/profile?seconds=30
# 2. 将 default.pgo 放到 main 包目录
# 3. 构建(自动启用 PGO)
go build -o app
# PGO 对热路径可提升 2-7%,主要改善 inlining 决策和代码布局
性能反模式清单
审查 Go 代码时,逐项检查:
- slice 在 append 前是否预分配了容量?
- map 是否预分配了初始大小?
- 循环中是否使用
strings.Builder而非+拼接? - 是否频繁进行
[]byte↔string转换(在热路径中)? - 小对象是否不需要返回指针(避免逃逸)?
- 不必要的 interface{} 是否用具体类型替代?
- struct 字段是否按大小排序以减少 padding?
- 并发热点是否使用了错误的锁类型(Mutex vs RWMutex)?
- 是否有 false sharing(多 goroutine 写相邻字段)?
- sync.Pool 是否用于高频临时对象?
- 热路径中的 defer 是否可以移除(Go 1.14+ 通常不需要)?
- 大的 goroutine 泄漏是否存在?
- 是否用
-race检测过数据竞争? - 是否确认了编译器完成了预期的 BCE 和内联?
- GC 配置(GOGC/GOMEMLIMIT)是否适合当前负载?
- 是否启用了 PGO?
快速诊断命令
# 1. 看哪些分配最多
go test -bench=. -benchmem -memprofile=mem.out
go tool pprof -top -alloc_space mem.out
# 2. 看哪些函数 CPU 最热
go test -bench=. -cpuprofile=cpu.out
go tool pprof -top cpu.out
# 3. 看逃逸分析
go build -gcflags="-m" ./... 2>&1 | grep "escapes to heap"
# 4. 看内联决策
go build -gcflags="-m -m" ./... 2>&1 | grep "inlining"
# 5. 看边界检查
go build -gcflags="-d=ssa/check_bce" ./...
# 6. 看 struct 对齐浪费
fieldalignment ./...
# 7. 看数据竞争
go test -race ./...
# 8. 看 GC 行为
GODEBUG=gctrace=1 ./app
# 9. benchstat 统计验证
go test -bench=. -count=10 > /tmp/bench.txt
benchstat /tmp/bench.txt
参考文件
需要更详细信息时加载:
references/benchmarking.md— 基准测试编写、benchstat 详解、常见反模式references/memory-optimization.md— 逃逸分析详解、分配减少策略、Pool 最佳实践references/cpu-optimization.md— BCE、内联、编译器 flag、汇编分析references/cache-optimization.md— CPU 缓存、false sharing、struct 布局references/concurrency-perf.md— 锁选择、分片、channel vs mutex、竞争分析references/tooling.md— pprof、trace、fieldalignment、benchstat 完整用法references/version-changes.md— Go 版本间关键性能变更详情references/pgo.md— Profile-Guided Optimization 完整工作流