GoのカスタムUnmarshalJSON vs 標準 性能対決
実際のベンチマークデータで見るJSONアンマーシャリング性能の深掘り
Goで複雑で動的なJSON構造を扱うとき、カスタムJSON Unmarshalロジックを書きたくなることがあります。
なので今回はカスタムUnmarshalを書く価値があるのかベンチマークを用いて性能比較してみました!
普段あんまり気にせずjson.Unmarshal
書いてる人は勉強になると思います!
課題
動的にネストされた構造を返すJSON APIを受け取り、レスポンスのデータを処理する必要がありました。
{
"id": "test-123",
"objectType": "UNIT",
"content": {
"unit": {
"url": "https://example.com/movie",
"checksum": "abc123"
}
}
}
content
フィールドにはobjectType
に応じて異なるサブオブジェクト(unit
、category
、item
)が含まれます。
上記課題に対して、二つのアプローチが考えました。
- カスタムUnmarshalJSON - 動的構造を処理するスマートなロジックを書く(想定外の動的な動きをした場合にコードの変更なしに対応できる)
- ユニオン構造体による標準JSON - Goの組み込みJSONパッケージにすべて任せる(想定外の動きには対応できない)
実装方法
アプローチ1:カスタムUnmarshalJSON
type Resource struct {
ID string `json:"id"`
ObjectType string `json:"objectType"`
Content Details // 単一のネストされた構造体
}
type Details struct {
Url string `json:"url"`
Checksum string `json:"checksum"`
}
func (r *Resource) UnmarshalJSON(data []byte) error {
var raw struct {
ID string `json:"id"`
ObjectType string `json:"objectType"`
Content map[string]json.RawMessage `json:"content"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
r.ID = raw.ID
r.ObjectType = raw.ObjectType
// objectTypeに基づく動的フィールド選択
objectTypeKey := strings.ToLower(raw.ObjectType)
contentData, exists := raw.Content[objectTypeKey]
if !exists {
return fmt.Errorf("missing '%s' field in content", objectTypeKey)
}
return json.Unmarshal(contentData, &r.Content)
}
アプローチ2:ユニオン構造体による標準JSON
type Item struct {
ID string `json:"id"`
ObjectType string `json:"objectType"`
Content ContentUnion `json:"content"`
}
type ContentUnion struct {
Item *ContentDetail `json:"item,omitempty"`
Category *ContentDetail `json:"category,omitempty"`
Unit *ContentDetail `json:"unit,omitempty"`
}
type ContentDetail struct {
Url string `json:"url"`
Checksum string `json:"checksum"`
}
// アクティブなcontentを取得するヘルパーメソッド
func (c *ContentUnion) GetActive() *ContentDetail {
if c.ObjectType == "unit" && c.Unit != nil {
return c.Unit
}
if c.ObjectType == "category" && c.Category != nil {
return c.Category
}
if c.ObjectType == "item" && c.Item != nil {
return c.Item
}
return nil
}
ベンチマーク結果
Goのテストフレームワークを使用して、両方のアプローチを包括的なベンチマークを実行しました。
goos: darwin
goarch: arm64
pkg: zeus/pkg/contents/canalplus
cpu: Apple M3
BenchmarkResource_CustomUnmarshal-8 86,374 14,365 ns/op 1,376 B/op 27 allocs/op
BenchmarkItem_StandardUnmarshal-8 200,640 8,301 ns/op 512 B/op 14 allocs/op
結果
標準JSONアプローチ(Item
)がカスタム実装(Resource
)を完全に上回りました。
考えればわかることなのですが、単純に処理が増えてるので、それは遅くなるはずです。
ただ倍以上パフォーマンスが違ったのは良い学びになりました。
- 🚀 1.73倍高速 な実行時間(8,301 ns vs 14,365 ns)
- 💾 2.69倍少ない メモリ使用量(512 B vs 1,376 B per operation)
- 🔄 1.93倍少ない アロケーション(14 vs 27 per operation)
- 📈 2.32倍高い スループット(200,640 vs 86,374 operations)
なぜ標準アプローチが勝つのか
1. GoのJSONパッケージは高度に最適化されている
encoding/json
パッケージは長年にわたって低レベルの最適化が施されており、カスタムコードでは太刀打ちできない。
2. JSON操作が少ない
- カスタム:2回の
json.Unmarshal
呼び出し + 文字列操作 + mapルックアップ - 標準:1回の
json.Unmarshal
呼び出しだけ
3. メモリプレッシャーが少ない
標準アプローチは構造体フィールドに直接マッピングしますが、カスタムロジックは中間mapを作成し、追加のアロケーションを実行する必要があります。
4. CPUキャッシュ効率
操作が少ないほど、CPUキャッシュの利用率が向上し、コンテキストスイッチが減少します。
Go哲学の実践
このベンチマークは、Goの哲学を完璧に実証しています:「シンプル is the best」。標準ライブラリには理由があり、ちょっとやそっとでは標準ライブラリに太刀打ちできません。
各アプローチを使用する場合
標準JSONを使用する場合:
- ✅ パフォーマンスが重要
- ✅ 構造体タグでデータをモデル化できる
- ✅ JSON構造がある程度予測可能
- ✅ 保守性を重視する
カスタムUnmarshalJSONを使用する場合
- ⚠️ JSON構造が極端に動的
- ⚠️ 複雑な検証ロジックが必要
- ⚠️ 標準JSONでは本当に処理できない
- ⚠️ 柔軟性の方がパフォーマンスより重要
Go開発者への重要なポイント
- すべてをベンチマークする - 数字が全て
- 標準ライブラリを信頼する - 原則、カスタム実装より高速
- シンプルなコードは高速なコード - 複雑さにはパフォーマンスコストがある
- ユニオン構造体は強力 - インプットが限られてる場合は、ある程度「動的」JSONシナリオを処理できる
結論
ついつい、いい感じに処理するカスタムJSON Unmarshal ロジックを書きたくなるかもしれませんが、パフォーマンスでは勝てません。ユニオン構造体を使用した標準JSONアプローチは、よりシンプルで保守しやすいだけでなく、大幅に高速です。
結果、基礎に戻るのですが、使えるなら標準ライブラリを使えに尽きます。
GoでのJSONパフォーマンスについて、似たような疑問を感じて検証したことありますか?ベンチマークで似たような経験したことありますか?コメントで教えてください!
Discussion