8月29日、Daniel Lemireが「The new Go JSON API: twice as fast, or 1.5x slower?」と題した記事を公開した。Go 1.27で標準ライブラリに正式追加された新JSONパッケージencoding/json/v2のパフォーマンスを、実際のベンチマークで検証した内容だ。
「新APIは2倍速い」という触れ込みは本当か——Lemireの計測が示す答えは「場合による」だ。ユースケースによっては旧APIの方が1.5倍速いという逆転現象が起きる。import文を1行変えるだけで乗り換えられそうに見えて、そう単純ではない。
なぜ今、新しいJSON APIなのか
encoding/jsonの設計上の限界は長年Goコミュニティで議論されてきた。リフレクションへの依存によるパフォーマンス上限、非互換な挙動、柔軟性の欠如——これらを解決すべく設計されたのがencoding/json/v2だ。Go 1.27でようやく標準ライブラリに取り込まれ、実用段階に入った。Lemireはsimdjsonやfastbase64など高速データ処理の研究で知られる計算機科学者で、パフォーマンス計測の信頼性は高い。
3つの比較対象
今回の計測では以下の3構成を比較している:
- json (legacy) —
GOEXPERIMENT=nojsonv2で旧実装を使ったencoding/json - json (Go 1.27) — Go 1.27の
encoding/json(v1 APIだが新エンジンで再実装) - json/v2 —
encoding/json/v2
APIの見た目はほぼ同じで、切り替えはimport文の変更だけだ:
import (
json "encoding/json"
jsonv2 "encoding/json/v2"
)
// 旧API
b, err := json.Marshal(v)
// 新API(既存のbとerrに再代入)
b, err = jsonv2.Marshal(v)
err = json.Unmarshal(b, &v)
err = jsonv2.Unmarshal(b, &v)
ただしドロップイン置き換えにはならない。Unicodeバリデーションの挙動、JSONキーの大文字小文字の扱いなど非互換な挙動変更があり、既存コードをそのまま差し替えるとサイレントに動作が変わるリスクがある。移行前に非互換点の公式ドキュメントでの確認が必須だ。
重要なのは、コードを1行も変えずにGo 1.27へアップグレードするだけで、encoding/jsonの内部エンジンが新実装に切り替わる点だ。GOEXPERIMENT=nojsonv2フラグで旧エンジンに戻すことも可能だ。
ベンチマーク結果:anyへのUnmarshalとMarshal
計測にはsimdjsonプロジェクトでも標準的に使われるJSONファイル3種を使用した:
twitter.json(632 kB、短い文字列キーを持つネストされたオブジェクト)canada.json(2.25 MB、座標の巨大配列)citm_catalog.json(1.73 MB、数値キーを持つネストされたオブジェクト)
環境はApple M4 MaxとIntel Xeon Gold 6548N(Emerald Rapids)、Go 1.27.0、シングルコア(GOMAXPROCS=1)、8回の中央値。
any(interface{})へのUnmarshalでは、コードを変えずGo 1.27へ移行するだけでも改善が見られる(twitter.jsonで172→203 MB/s、citm_catalog.jsonで186→241 MB/s)。一方、canada.jsonでは128→106 MB/sと後退するケースもある。
Marshalについては、コードを変えずGo 1.27へ移行するだけで最大2倍の高速化が得られる(twitter.jsonで198→374 MB/s)。さらにencoding/json/v2に切り替えると、旧実装比でUnmarshalが1.5〜2.3倍、Marshalが1.2〜3倍速くなる。
落とし穴:型付き構造体のMarshalは逆に遅くなる
ここが記事の核心だ。上記の結果はany(スキーマ不明)の場合だ。実際のプロダクションコードで多用する、スキーマが決まった構造体では話が変わる。
type Record struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Active bool `json:"active"`
Score float64 `json:"score"`
Tags []string `json:"tags"`
}
この構造体10,000件のスライスでラウンドトリップを計測した結果:
- Unmarshalは新APIでも速くなる
- Marshalは旧エンジン+旧APIが最速、新APIは約1.5倍遅い
Go 1.27のリリースノートには「Marshalのパフォーマンスは旧実装と概ね同等」とあるが、この計測では一致しなかったとLemireは述べている。リフレクションで型情報を読み取る旧実装が、構造体の固定スキーマという条件下では有利に働く、という皮肉な結果だ。
何を選ぶか
結果を整理するとこうなる:
| ケース | 推奨 |
|---|---|
| コードを変えずにGo 1.27へ移行 | Marshal系は恩恵あり(canada.jsonのUnmarshalは速度後退の例外あり) |
anyへのUnmarshal重視 |
json/v2への切り替えが明確に有利(最大2.3倍) |
| 型付き構造体のMarshal重視 | 旧APIのまま維持、またはGOEXPERIMENT=nojsonv2で旧エンジンに戻す |
自分のアプリケーションの主なJSONユースケースが何か(anyか構造体か、MarshalかUnmarshalか)を確認した上で判断したい。ベンチマークコードはGitHubで公開されており、自分のユースケースに近い条件で追試できる。
詳細はThe new Go JSON API: twice as fast, or 1.5x slower?を参照していただきたい。

