GCが全CPU時間の約70%を消費 (通常は5-20%が適正範囲)
頻繁な小規模オブジェクトのアロケーション
runtime.mallocgc
とruntime.mallocgcSmallScanNoHeader
が高頻度で呼ばれる
文字列操作(concatstrings
、convTstring
)による過度のアロケーション
evaluator.Evalのコード分析から判明したホットスポット
デバッグログでの文字列生成 (evaluator.go:259-275)
毎回bytes.Buffer
を作成
printer.Fprint
による文字列化
ログレベルに関わらず文字列生成が発生する可能性
大きなswitch文 (evaluator.go:277-405)
エラーオブジェクトの生成
頻繁なnewError
呼び出しによるフォーマット文字列生成
// benchmark_test.go
func BenchmarkEvaluator (b * testing.B ) {
// 典型的なワークロードを実行
// メモリアロケーション数を測定
}
# アロケーションプロファイル
go test -bench=. -memprofile=mem.prof
go tool pprof -alloc_space mem.prof
# ヒーププロファイル
go test -bench=. -memprofile=heap.prof
go tool pprof -inuse_space heap.prof
問題 : ログレベルチェック後も文字列生成が発生
// 現在のコード(毎回Bufferを作成)
if e .logger .Enabled (ctx , slog .LevelDebug ) {
var buf bytes.Buffer // 毎回アロケーション
printer .Fprint (& buf , fset , node )
}
改善案 :
// Buffer poolの導入
var bufferPool = sync.Pool {
New : func () interface {} {
return new (bytes.Buffer )
},
}
if e .logger .Enabled (ctx , slog .LevelDebug ) {
buf := bufferPool .Get ().(* bytes.Buffer )
buf .Reset ()
defer bufferPool .Put (buf )
printer .Fprint (buf , fset , node )
}
問題 : 頻繁なfmt.Sprintf
や文字列連結
// 現在のコード
return fmt .Sprintf ("package.%s.%s" , pkg .Name , ident .Name )
改善案 :
// strings.Builderを使用
var sb strings.Builder
sb .WriteString ("package." )
sb .WriteString (pkg .Name )
sb .WriteByte ('.' )
sb .WriteString (ident .Name )
return sb .String ()
// Object poolの導入
type ObjectPool struct {
variablePool sync.Pool
placeholderPool sync.Pool
errorPool sync.Pool
}
func (p * ObjectPool ) GetVariable () * object.Variable {
v := p .variablePool .Get ()
if v == nil {
return & object.Variable {}
}
return v .(* object.Variable )
}
func (p * ObjectPool ) PutVariable (v * object.Variable ) {
// Reset fields
v .Name = ""
v .Value = nil
p .variablePool .Put (v )
}
現在 : すべてのobjectがポインタで扱われる
改善 : 小さな構造体(16バイト以下)は値型で扱う
フェーズ4: マップとスライスの最適化(4-8時間)
// 現在のコード
m := make (map [string ]object.Object )
// 改善後(サイズが予測可能な場合)
m := make (map [string ]object.Object , expectedSize )
// 現在のコード
var results []object.Object
for _ , item := range items {
results = append (results , process (item )) // 複数回の再アロケーション
}
// 改善後
results := make ([]object.Object , 0 , len (items ))
for _ , item := range items {
results = append (results , process (item ))
}
フェーズ5: キャッシングとメモ化の強化(8-16時間)
type MemoizationCache struct {
mu sync.RWMutex
cache map [memoKey ]memoValue
}
type memoKey struct {
fnPtr uintptr
argsHash uint64
}
type memoValue struct {
result object.Object
hits int
}
// 型解決結果をキャッシュ
typeCache := make (map [ast.Expr ]* goscan.TypeInfo )
# GC頻度を下げる
GOGC=200 ./find-orphans
# メモリ制限を設定
GOMEMLIMIT=2GiB ./find-orphans
# 両方を組み合わせる
GOGC=200 GOMEMLIMIT=2GiB ./find-orphans
# プロファイルを収集
go build -o find-orphans
./find-orphans -cpuprofile=default.pgo
# PGOを使用してビルド
go build -pgo=default.pgo -o find-orphans-optimized
フェーズ
実装時間
期待される改善
リスク
1. 測定環境構築
1-2時間
-
なし
2. Quick Wins
2-4時間
10-20%
低
3. 構造体最適化
4-8時間
20-30%
中
4. マップ/スライス
4-8時間
10-15%
低
5. キャッシング
8-16時間
30-40%
中
6. GCチューニング
2-4時間
10-20%
低
総合的な期待効果 : 2-3倍の高速化
# 最適化前
time ./find-orphans --mode lib --workspace-root . ./symgo/evaluator
# 最適化後
time ./find-orphans-optimized --mode lib --workspace-root . ./symgo/evaluator
# メモリ統計を有効にして実行
GODEBUG=gctrace=1 ./find-orphans 2>&1 | grep gc
func TestAllocations (t * testing.T ) {
allocs := testing .AllocsPerRun (100 , func () {
// テストケースを実行
})
t .Logf ("Allocations per run: %.0f" , allocs )
}
APIの変更は最小限に
既存のテストがすべてパスすることを確認
sync.Poolは並行安全だが、オブジェクトの状態管理に注意
キャッシュのrace conditionを避ける
Poolに返却する前にオブジェクトをリセット
循環参照を避ける
GCのCPU使用率を70%から20%以下に削減
実行時間を50%以上短縮
メモリ使用量のピークを30%削減
アロケーション数を60%削減
各フェーズごとにgitでタグ付けし、問題が発生した場合は前のバージョンに戻せるようにする。
git tag -a v1.0-before-optimization
git tag -a v1.1-after-phase2
git tag -a v1.2-after-phase3
# など
結局メモ化がうまく動いていないというだけの話だった