[インデックス 11477] ファイルの概要
このコミットは、Go言語の標準ライブラリである encoding/json
パッケージ内の scanner.go
ファイルに対する変更です。具体的には、JSONスキャン処理におけるスペース文字の判定ロジックから、かつてパフォーマンス最適化のために手動で展開されていた条件式を削除し、isSpace
関数への呼び出しに置き換えるものです。これは、Goコンパイラのインライン化能力の向上により、手動最適化が不要になったことを示しています。
コミット
commit 25c96cba2e6e9729ffda9e05da05f121ce9077f4
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Jan 30 11:42:09 2012 -0800
json: remove old optimization that inlining covers now
Benchmarks look the same.
R=golang-dev, dsymonds, rsc
CC=golang-dev
https://golang.org/cl/5572080
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/25c96cba2e6e9729ffda9e05da05f121ce9077f4
元コミット内容
このコミットの目的は、「インライン化によってカバーされるようになった古い最適化を削除する」ことです。ベンチマークの結果は変わらないとされており、これはコンパイラの進化によってコードの可読性を損なう手動最適化が不要になったことを示唆しています。
変更の背景
Go言語の初期のコンパイラ(特に 6g
と呼ばれる当時のGoコンパイラ)は、関数のインライン化に関して現在ほど高度ではありませんでした。インライン化とは、関数呼び出しのオーバーヘッドを避けるために、呼び出し元のコードに関数本体を直接埋め込むコンパイラ最適化の一種です。
encoding/json
パッケージのようなパフォーマンスが重視されるコードでは、小さな関数(例えばスペース文字を判定する isSpace
関数)の呼び出しであっても、そのオーバーヘッドが全体のパフォーマンスに影響を与える可能性がありました。そのため、このコミットが変更する前のコードでは、isSpace
関数を呼び出す代わりに、スペース文字の判定ロジック c == ' ' || c == '\\t' || c == '\\r' || c == '\\n'
を各所で直接記述するという、手動での「インライン化」が行われていました。これは、当時のコンパイラが isSpace
のような単純な関数を自動的にインライン化できなかったため、開発者がパフォーマンスのために明示的にコードを展開していたものです。
しかし、Goコンパイラは継続的に改善され、より賢く、より多くのケースで自動的にインライン化を行うようになりました。このコミットは、そのコンパイラの進化を反映したものであり、isSpace
関数の呼び出しがコンパイラによって効率的にインライン化されるようになったため、手動でのコード展開が不要になったと判断されました。これにより、コードの重複が減り、可読性が向上します。ベンチマークの結果が変わらないことは、この変更がパフォーマンスに悪影響を与えないことを裏付けています。
前提知識の解説
- Go言語の
encoding/json
パッケージ: Go言語の標準ライブラリの一部で、JSONデータのエンコード(Goのデータ構造からJSONへ)とデコード(JSONからGoのデータ構造へ)を提供します。内部的には、JSON文字列を解析するためのスキャナー(scanner.go
)が含まれています。 - JSONスキャナー: JSONデータをバイトストリームまたは文字ストリームとして読み込み、JSONのトークン(文字列、数値、ブール値、null、オブジェクトの開始/終了、配列の開始/終了など)を識別するコンポーネントです。スペース文字のスキップは、JSONの構文解析において非常に頻繁に行われる操作です。
- コンパイラのインライン化 (Inlining): プログラムの最適化手法の一つで、関数呼び出しのオーバーヘッド(スタックフレームの作成、引数のプッシュ、戻りアドレスの保存など)を削減するために、呼び出される関数のコードを呼び出し元の場所に直接挿入することです。これにより、実行時のパフォーマンスが向上する可能性がありますが、コードサイズが増加する可能性もあります。現代のコンパイラは、ヒューリスティックに基づいてどの関数をインライン化するかを決定します。
rune
型: Go言語におけるUnicodeコードポイントを表す型です。int32
のエイリアスであり、文字を扱う際に使用されます。isSpace
関数: 一般的に、空白文字(スペース、タブ、改行、キャリッジリターンなど)を判定するためのヘルパー関数です。JSONの仕様では、特定の場所で空白文字が許容されており、スキャナーはこれらをスキップする必要があります。
技術的詳細
このコミットの核心は、Goコンパイラのインライン化能力の進化にあります。
変更前のコードには、以下のようなコメントがありました。
// NOTE(rsc): The various instances of
//
// if c <= ' ' && (c == ' ' || c == '\t' || c == '\r' || c == '\n')
//
// below should all be if c <= ' ' && isSpace(c), but inlining
// the checks makes a significant difference (>10%) in tight loops
// such as nextValue. These should be rewritten with the clearer
// function call once 6g knows to inline the call.
このコメントは、当時のGoコンパイラ 6g
が isSpace(c)
のような関数呼び出しをインライン化できなかったため、パフォーマンス上の理由から、isSpace
関数のロジック(c == ' ' || c == '\\t' || c == '\\r' || c == '\\n'
)を各所で手動で展開していたことを明確に示しています。コメントの最後には、「6g
が呼び出しをインライン化できるようになれば、より明確な関数呼び出しで書き直すべきだ」と明記されており、まさにこのコミットはその「書き直し」を実行したものです。
変更後のコードでは、手動で展開されていたスペース判定ロジックが isSpace(rune(c))
の呼び出しに置き換えられています。これは、Goコンパイラが isSpace
のような単純な関数を自動的にインライン化するようになったため、関数呼び出しのオーバーヘッドが無視できるレベルになったことを意味します。結果として、コードはより簡潔になり、isSpace
という意味論的に適切な関数名を使用することで、可読性と保守性が向上しました。
ベンチマークの結果が「変わらない」とされていることは、この変更がパフォーマンスリグレッションを引き起こさなかったことを示しており、コンパイラの最適化が期待通りに機能していることの証拠となります。
コアとなるコードの変更箇所
src/pkg/encoding/json/scanner.go
ファイルにおいて、以下のパターンで記述されていた部分が変更されています。
--- a/src/pkg/encoding/json/scanner.go
+++ b/src/pkg/encoding/json/scanner.go
@@ -185,18 +185,9 @@ func isSpace(c rune) bool {
return c == ' ' || c == '\t' || c == '\r' || c == '\n'
}
-// NOTE(rsc): The various instances of
-//
-//
-// if c <= ' ' && (c == ' ' || c == '\t' || c == '\r' || c == '\n')
-//
-// below should all be if c <= ' ' && isSpace(c), but inlining
-// the checks makes a significant difference (>10%) in tight loops
-// such as nextValue. These should be rewritten with the clearer
-// function call once 6g knows to inline the call.
-
// stateBeginValueOrEmpty is the state after reading `[`.
func stateBeginValueOrEmpty(s *scanner, c int) int {
- if c <= ' ' && (c == ' ' || c == '\t' || c == '\r' || c == '\n') {
+ if c <= ' ' && isSpace(rune(c)) {
return scanSkipSpace
}
if c == ']' {
@@ -207,7 +198,7 @@ func stateBeginValueOrEmpty(s *scanner, c int) int {
// stateBeginValue is the state at the beginning of the input.
func stateBeginValue(s *scanner, c int) int {
- if c <= ' ' && (c == ' ' || c == '\t' || c == '\r' || c == '\n') {
+ if c <= ' ' && isSpace(rune(c)) {
return scanSkipSpace
}
switch c {
@@ -247,7 +238,7 @@ func stateBeginValue(s *scanner, c int) int {
// stateBeginStringOrEmpty is the state after reading `{`.
func stateBeginStringOrEmpty(s *scanner, c int) int {
- if c <= ' ' && (c == ' ' || c == '\t' || c == '\r' || c == '\n') {
+ if c <= ' ' && isSpace(rune(c)) {
return scanSkipSpace
}
if c == '}' {
@@ -260,7 +251,7 @@ func stateBeginStringOrEmpty(s *scanner, c int) int {
// stateBeginString is the state after reading `{"key": value,`.
func stateBeginString(s *scanner, c int) int {
- if c <= ' ' && (c == ' ' || c == '\t' || c == '\r' || c == '\n') {
+ if c <= ' ' && isSpace(rune(c)) {
return scanSkipSpace
}
if c == '"' {
@@ -280,7 +271,7 @@ func stateEndValue(s *scanner, c int) int {
s.endTop = true
return stateEndTop(s, c)
}
- if c <= ' ' && (c == ' ' || c == '\t' || c == '\r' || c == '\n') {
+ if c <= ' ' && isSpace(rune(c)) {
s.step = stateEndValue
return scanSkipSpace
}
コアとなるコードの解説
変更は、scanner.go
内の複数の状態関数(stateBeginValueOrEmpty
, stateBeginValue
, stateBeginStringOrEmpty
, stateBeginString
, stateEndValue
)におけるスペース文字のスキップロジックに集中しています。
変更前は、以下のような条件式が直接記述されていました。
if c <= ' ' && (c == ' ' || c == '\\t' || c == '\\r' || c == '\\n')
これは、文字 c
がASCIIのスペース文字(' '
)以下であり、かつそれが実際にスペース、タブ、キャリッジリターン、改行のいずれかであるかをチェックしています。c <= ' '
という条件は、これらの空白文字がASCIIコードでスペース文字以下に位置するという特性を利用した、わずかな最適化です。
変更後は、この部分が以下のように簡潔になりました。
if c <= ' ' && isSpace(rune(c))
ここで、isSpace
関数は scanner.go
の同じファイル内で定義されており、その内容は以下の通りです。
func isSpace(c rune) bool {
return c == ' ' || c == '\t' || c == '\r' || c == '\n'
}
この変更により、コードの意図がより明確になりました。「もし c
がスペース文字以下であり、かつ isSpace
関数が真を返す(つまり、c
が空白文字である)ならば」というロジックが、関数呼び出しによって表現されています。
rune(c)
へのキャストは、c
が int
型で渡されてくるのに対し、isSpace
関数が rune
型の引数を期待するためです。Goでは、文字リテラルは rune
型であり、int
と rune
は互換性がありますが、明示的なキャストは型安全性を高めます。
この変更は、機能的には全く同じ動作をしますが、コンパイラのインライン化能力の向上により、手動でのコード展開が不要になり、コードの重複が排除され、保守性が向上したという点で非常に重要です。
関連リンク
- Go CL 5572080: https://golang.org/cl/5572080 (このコミットに対応するGoのコードレビューシステム上のチェンジリスト)
- Go言語の
encoding/json
パッケージのドキュメント: https://pkg.go.dev/encoding/json
参考にした情報源リンク
- Go言語のコンパイラ最適化に関する一般的な情報 (Goのインライン化ポリシーなど):
- Goのインライン化に関する議論やドキュメントは、Goの公式ブログやGoのソースコードリポジトリのドキュメント(例:
src/cmd/compile/internal/inline/inline.go
のコメントなど)に散見されます。 - Goのコンパイラがどのように進化してきたかについては、GoのリリースノートやGoの設計に関する論文(例: "The Go Programming Language Specification" や "Go: a new type-safe, garbage-collected, concurrent system programming language")が参考になります。
- Goのインライン化に関する議論やドキュメントは、Goの公式ブログやGoのソースコードリポジトリのドキュメント(例:
- JSONの仕様 (RFC 8259): https://www.rfc-editor.org/rfc/rfc8259 (JSONにおける空白文字の扱いについて)
- Go言語の
rune
型について: https://go.dev/blog/strings (Goにおける文字列と文字の扱いに関する公式ブログ記事) - Goの初期のコンパイラ(
6g
など)に関する情報: Goの古いドキュメントやメーリングリストのアーカイブに情報が見られます。