【Go言語学習|実務向け】Goのパフォーマンスチューニングの第一歩:benchmemでアロケーションを可視化する

1. 導入

Go言語はガベージコレクション(GC)を備えた言語ですが、メモリのアロケーション(確保)が多すぎると、GCの実行頻度が高まり、アプリケーションのレイテンシやスループットに悪影響を及ぼします。「なんとなく遅い」と感じる処理がある場合、まずはメモリの確保状況を定量化することが最適化の近道です。今回は、Go標準のベンチマーク機能である「go test -benchmem」を活用して、メモリ効率を可視化する方法を解説します。

2. 基礎知識

Goにおけるメモリ確保は、基本的にヒープ領域に対して行われます。関数内で作成された変数がスコープを抜けた後も参照される場合、その変数はヒープに配置されます。これを「エスケープ」と呼びます。
アロケーション密度が高いということは、短期間に大量のオブジェクトが生成されていることを意味し、GCがメモリを回収する際の負荷(Stop The Worldの時間など)を増大させます。benchmemを使用することで、1回の処理あたりに何回メモリ確保が発生し、合計で何バイト確保されたかを数値として把握できます。

3. 実装/解決策

ベンチマークを実行する際は、通常のテストコードと同様に「_test.go」ファイル内に「Benchmark」から始まる関数を記述します。
実行コマンドは以下の通りです。

go test -bench . -benchmem

このコマンドを叩くことで、標準出力に「ns/op(1操作あたりの実行時間)」「B/op(1操作あたりの確保バイト数)」「allocs/op(1操作あたりの確保回数)」が表示されます。特に「allocs/op」を減らすことが、メモリ効率改善の大きな指標となります。

4. サンプルプログラム

以下は、文字列の結合処理において、メモリ確保回数が異なる2つのパターンを比較する例です。

package main

import (
“strings”
“testing”
)

// パターン1: +演算子による結合(メモリ確保が頻発する)
func BenchmarkStringConcat(b testing.B) {
for i := 0; i < b.N; i++ { var s string for j := 0; j < 100; j++ { s += "a" // ループのたびに新しい文字列が生成され、メモリ確保が走る } } } // パターン2: strings.Builderの使用(バッファを再利用するため効率的) func BenchmarkStringBuilder(b testing.B) { for i := 0; i < b.N; i++ { var sb strings.Builder for j := 0; j < 100; j++ { sb.WriteString("a") // 内部バッファを再利用し、アロケーションを最小限に抑える } _ = sb.String() } } // 実行方法: go test -bench . -benchmem

5. 応用・注意点

現場で活用する際のポイントは以下の3点です。

・ベンチマークのノイズを避ける
複雑な処理を計測する場合、処理前後の初期化処理が計測に含まれないよう「b.ResetTimer()」を適切に呼び出してください。

・コンパイラの最適化に注意
ベンチマーク対象の結果をどこにも出力しないと、コンパイラが「不要な処理」と判断してコードを削除し、結果が0になることがあります。結果を外部変数に代入するなどの工夫が必要です。

・過度な最適化の罠
メモリ確保を極端に減らすために複雑なコード(例:メモリプールやunsafeパッケージの多用)を書くと、可読性が著しく低下します。まずは「-benchmem」でボトルネックを特定し、パフォーマンスが本当に必要な箇所に対してのみ最適化を行うのが、Goらしい保守性の高い開発スタイルです。

コメント

タイトルとURLをコピーしました