[インデックス 13800] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)において、ポインタサイズの型からインターフェースへの変換(T2I変換)をインライン化することで、パフォーマンスを大幅に向上させることを目的としています。また、AST(抽象構文木)内のIF
ノードに「可能性(likelyness)」のヒントを付与する機能を追加し、コンパイラが条件分岐の最適化をより効果的に行えるようにしています。特に、インターフェース変換における「遅いパス(slow path)」を「可能性が低い(unlikely)」とマークすることで、コンパイラがより効率的なコードを生成できるようになります。
コミット
commit 8fd65b0e1d6b737072982a1cf846ff76b1353f02
Author: Daniel Morsing <daniel.morsing@gmail.com>
Date: Tue Sep 11 21:42:30 2012 +0200
cmd/gc: Inline pointer sized T2I interface conversions
This CL also adds support for marking the likelyness of IF nodes in the AST being true. This feature is being used here to mark the slow path as unlikely.
src/pkg/runtime:
benchmark old ns/op new ns/op delta
BenchmarkConvT2IUintptr 16 1 -91.63%
test/bench/go1:
benchmark old ns/op new ns/op delta
BenchmarkBinaryTree17 5416917000 5461355000 +0.82%
BenchmarkFannkuch11 3810355000 3842609000 +0.85%
BenchmarkGobDecode 19950950 19855420 -0.48%
BenchmarkGobEncode 11301220 11308530 +0.06%
BenchmarkGzip 548119600 546869200 -0.23%
BenchmarkGunzip 176145400 180208300 +2.31%
BenchmarkJSONEncode 93117400 70163100 -24.65%
BenchmarkJSONDecode 406626800 409999200 +0.83%
BenchmarkMandelbrot200 6300992 6317866 +0.27%
BenchmarkParse 7664396 7451625 -2.78%
BenchmarkRevcomp 1189424000 1412332000 +18.74%
BenchmarkTemplate 491308400 458654200 -6.65%
benchmark old MB/s new MB/s speedup
BenchmarkGobDecode 38.47 38.66 1.00x
BenchmarkGobEncode 67.92 67.87 1.00x
BenchmarkGzip 35.40 35.48 1.00x
BenchmarkGunzip 110.16 107.68 0.98x
BenchmarkJSONEncode 20.84 27.66 1.33x
BenchmarkJSONDecode 4.77 4.73 0.99x
BenchmarkParse 7.56 7.77 1.03x
BenchmarkRevcomp 213.69 179.96 0.84x
BenchmarkTemplate 3.95 4.23 1.07x
R=rsc, dave, nigeltao
CC=golang-dev
https://golang.org/cl/6351090
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8fd65b0e1d6b737072982a1cf846ff76b1353f02
元コミット内容
このコミットの主な目的は、Goコンパイラ(cmd/gc
)において、ポインタサイズの型(例: *int
, string
, []byte
など)からインターフェース型への変換(T2I変換)の性能を向上させることです。具体的には、これらの変換処理をインライン化することで、関数呼び出しのオーバーヘッドを削減し、実行速度を高速化します。
さらに、このコミットでは、AST(抽象構文木)内のIF
ノード(条件分岐)に対して、その条件が真になる「可能性(likelyness)」をマークする機能が追加されています。この機能は、コンパイラが条件分岐の最適化を行う際に、どちらのパスがより頻繁に実行されるかを予測するのに役立ちます。今回の変更では、特にインターフェース変換における「遅いパス」を「可能性が低い(unlikely)」とマークすることで、コンパイラがより効率的なコードを生成し、高速なパスに最適化のリソースを集中させることが可能になります。
ベンチマーク結果を見ると、BenchmarkConvT2IUintptr
が91.63%という劇的な改善を示しており、この最適化がインターフェース変換の性能に大きな影響を与えていることがわかります。他のベンチマークでは、BenchmarkJSONEncode
が24.65%の改善、BenchmarkTemplate
が6.65%の改善を見せていますが、BenchmarkRevcomp
のように18.74%の性能低下が見られるものもあります。これは、コンパイラの最適化が全体的なパフォーマンスに与える影響が複雑であることを示唆しています。
変更の背景
Go言語は、その設計思想としてシンプルさとパフォーマンスの両立を目指しています。インターフェースはGoの強力な機能の一つであり、ポリモーフィズムを実現するために広く利用されています。しかし、インターフェースの内部表現と、具体的な型からインターフェースへの変換(T2I変換)には、実行時コストが伴います。特に、ポインタサイズの型は頻繁にインターフェースに変換されるため、この部分のオーバーヘッドがアプリケーション全体のパフォーマンスに影響を与える可能性がありました。
このコミットが行われた2012年当時、Goコンパイラはまだ初期段階にあり、様々な最適化の余地がありました。インターフェース変換のインライン化は、関数呼び出しのコストを削減し、より効率的な機械語コードを生成するための一般的なコンパイラ最適化手法です。
また、条件分岐の「可能性」をコンパイラに伝える機能は、プロファイルに基づく最適化(PGO)や、静的なコード解析に基づくヒューリスティックな最適化において非常に重要です。コンパイラがどのパスが「ホットパス」(頻繁に実行されるパス)であるかを事前に知っていれば、そのパスに特化した最適化を施し、全体の実行速度を向上させることができます。インターフェース変換の文脈では、型が既にインターフェースに適合している場合の「速いパス」と、適合していない場合の「遅いパス」(ランタイムでの型チェックやItab
検索が必要なパス)が存在します。この「遅いパス」を「unlikely」とマークすることで、コンパイラは「速いパス」を優先的に最適化し、全体的な性能向上を図ることができます。
前提知識の解説
GoのインターフェースとT2I変換
Goのインターフェースは、メソッドのシグネチャの集合を定義する型です。Goのインターフェースは、動的なディスパッチを可能にするために、内部的には2つのワードで構成される構造体として表現されます。
- 型情報(Type Word): インターフェースに格納されている具体的な値の型情報(
_type
構造体へのポインタ)。 - 値情報(Value Word): インターフェースに格納されている具体的な値そのもの(またはその値へのポインタ)。
**T2I変換(Type-to-Interface Conversion)**とは、具体的な型(例: int
, string
, MyStruct
など)の値をインターフェース型に代入する際に発生する変換処理です。この変換では、ランタイムが具体的な型の情報と値情報を抽出し、インターフェースの内部構造に格納します。
ポインタサイズの型(例: *int
, string
, []byte
)の場合、値情報は直接インターフェースのValue Wordに格納されます。しかし、値がポインタサイズでない場合(例: int
, struct{}
など)、値はヒープにアロケートされ、そのポインタがValue Wordに格納されます。
T2I変換のプロセスには、以下のステップが含まれます。
- 型チェック: 具体的な型がインターフェースのすべてのメソッドを実装しているかどうかのチェック。
Itab
(Interface Table)の検索/生成: インターフェース型と具体的な型のペアに対応するItab
を探します。Itab
は、その具体的な型がインターフェースのメソッドをどのように実装しているか(メソッドテーブル)の情報を含んでいます。Itab
はキャッシュされ、同じペアの変換が再度行われる際に再利用されます。- 値の格納: 具体的な値をインターフェースのValue Wordに格納します。
これらのステップは、特にItab
の検索やヒープアロケーション(値がポインタサイズでない場合)がパフォーマンスのボトルネックとなることがあります。
AST(抽象構文木)とコンパイラ最適化
**AST(Abstract Syntax Tree)**は、ソースコードの構造を木構造で表現したものです。コンパイラは、ソースコードをパースしてASTを生成し、このASTに対して様々な解析や変換(最適化)を行います。
コンパイラ最適化は、生成される機械語コードの実行速度やサイズを改善するためのプロセスです。これには、以下のような様々な手法があります。
- インライン化(Inlining): 関数呼び出しを、呼び出し元のコードに直接展開することで、関数呼び出しのオーバーヘッド(スタックフレームのセットアップ、レジスタの保存/復元など)を削減します。
- デッドコード削除(Dead Code Elimination): 決して実行されないコードを削除します。
- 定数畳み込み(Constant Folding): コンパイル時に計算可能な式を事前に計算し、その結果で置き換えます。
- ループ最適化(Loop Optimization): ループの実行を高速化するための様々な手法(ループアンローリング、ループ不変コードの移動など)。
条件分岐の「可能性(Likelyness)」ヒント
コンパイラは、if
文やswitch
文のような条件分岐の最適化を行う際に、どちらの分岐がより頻繁に実行されるかを知っていると、より効率的なコードを生成できます。例えば、頻繁に実行されるパス(ホットパス)を、CPUのキャッシュに乗りやすいように配置したり、分岐予測器が正しく予測できるようにコードを並べ替えたりすることができます。
この「可能性」のヒントは、以下のような方法でコンパイラに与えられます。
- プロファイルに基づく最適化(PGO: Profile-Guided Optimization): 実際のプログラム実行時のプロファイルデータ(どのコードパスがどれくらいの頻度で実行されたか)を収集し、それに基づいて最適化を行います。
- 静的なヒューリスティック: コードのパターンや特定のAPIの使用状況から、コンパイラが経験的に「この条件は真になりやすい/なりにくい」と判断します。
このコミットでは、後者の静的なヒューリスティックに近い形で、ASTノードにlikely
というフィールドを追加し、コンパイラが明示的に条件の可能性をマークできるようにしています。likely = -1
は「unlikely」(可能性が低い)を意味し、コンパイラはそれに応じて最適化戦略を調整します。
技術的詳細
このコミットの技術的詳細は、主に以下の2つの側面から構成されます。
-
ポインタサイズのT2I変換のインライン化:
- Goのインターフェース変換は、通常、ランタイム関数(例:
runtime.convT2I
)への呼び出しを伴います。この関数は、型チェック、Itab
の検索/生成、値の格納といった処理を行います。 - ポインタサイズの型の場合、値のコピーが比較的単純であり、ヒープアロケーションも不要なため、この変換処理をコンパイラが直接生成するコード(インラインコード)に置き換えることで、関数呼び出しのオーバーヘッドを排除し、大幅な高速化が期待できます。
- インライン化されたコードは、
Itab
のキャッシュを直接参照し、もしキャッシュミスが発生した場合は、runtime.typ2Itab
という新しいランタイム関数を呼び出してItab
を検索/生成し、キャッシュを更新します。この「キャッシュミス時のランタイム呼び出し」が「遅いパス」となります。
- Goのインターフェース変換は、通常、ランタイム関数(例:
-
ASTにおける
IF
ノードのlikelyness
マーキング:src/cmd/gc/go.h
のNode
構造体にschar likely;
フィールドが追加されました。これは、if
文の条件が真になる可能性を示すために使用されます。likely
の値は、0
(不明)、1
(likely、可能性が高い)、-1
(unlikely、可能性が低い)などを表すと考えられます。src/cmd/gc/gen.c
では、bgen
関数(条件分岐のコード生成を行う関数)がn->likely
フィールドを考慮するように変更されました。これにより、コンパイラは条件分岐のコードを生成する際に、その条件が真になる可能性に基づいて、より効率的な分岐命令やコード配置を選択できるようになります。- 今回のT2I変換の最適化では、インライン化されたコード内で
Itab
キャッシュがnil
であるかどうかのチェック(つまり、キャッシュミスが発生した場合)のif
文に対して、n2->likely = -1;
と設定されています。これは、キャッシュミスが頻繁に発生するわけではないため、このパスが「unlikely」であることをコンパイラに明示的に伝えています。これにより、コンパイラはキャッシュヒットの「速いパス」を優先的に最適化し、キャッシュミスの「遅いパス」にはあまりリソースを割かないようにします。
Itab
とruntime.typ2Itab
Goのインターフェースは、内部的にItab
(Interface Table)という構造体を利用して、具体的な型がインターフェースのメソッドをどのように実装しているかを管理します。Itab
は、インターフェース型と具体的な型のペアごとに一意に存在し、その型が実装するメソッドへのポインタの配列を含んでいます。
T2I変換の際、ランタイムはまず、変換対象の具体的な型とインターフェース型に対応するItab
が既に存在するかどうかをキャッシュで検索します。存在すればそれを利用し、存在しなければ新しく生成してキャッシュに格納します。
このコミットで追加されたruntime.typ2Itab
関数は、このItab
の検索と生成、そしてキャッシュへの格納を行うランタイム関数です。インライン化されたT2I変換コードは、キャッシュミスが発生した場合にこのruntime.typ2Itab
を呼び出すことで、必要なItab
を取得します。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下のファイルに集中しています。
-
src/cmd/gc/builtin.c
:runtimeimport
文字列に新しいランタイム関数typ2Itab
のシグネチャが追加されています。これは、コンパイラがこのランタイム関数を認識し、呼び出せるようにするためです。
+ "func @\"\\\".typ2Itab(@\"\\\".typ *byte, @\"\\\".typ2 *byte, @\"\\\".cache **byte) (@\"\\\".ret *byte)\\n"
-
src/cmd/gc/gen.c
:gen
関数内のbgen
呼び出しで、n->likely
フィールドが使用されるように変更されています。これにより、条件分岐のコード生成時にlikelyness
ヒントが考慮されます。
- bgen(n->ntest, 0, 0, p2); // if(!test) goto p2 + bgen(n->ntest, 0, -n->likely, p2); // if(!test) goto p2
cgen_eface
関数(インターフェース値のコード生成)のロジックが変更され、n->left
とn->right
の評価順序が入れ替わっています。これは、eface
の右側のノードがres
を引数として使用する関数呼び出しを含む可能性があるため、res
が先に準備されるようにするためです。
- cgen(n->left, &dst); dst.xoffset += widthptr; cgen(n->right, &dst); + dst.xoffset -= widthptr; + cgen(n->left, &dst);
-
src/cmd/gc/go.h
:Node
構造体にschar likely;
フィールドが追加されています。これは、if
文の条件が真になる可能性をマークするために使用されます。
+ schar likely; // likeliness of if statement
-
src/cmd/gc/runtime.go
:typ2Itab
ランタイム関数のGo側の宣言が追加されています。これは、コンパイラがこの関数をGoコードから呼び出せるようにするためです。
+func typ2Itab(typ *byte, typ2 *byte, cache **byte) (ret *byte)
-
src/cmd/gc/walk.c
:walkexpr
関数内で、ポインタサイズのT2I変換に対する特殊な最適化ロジックが追加されています。- このロジックは、
Itab
キャッシュを直接参照し、キャッシュミスの場合にruntime.typ2Itab
を呼び出すコードを生成します。 - 特に重要なのは、キャッシュミスをチェックする
if
文に対してn2->likely = -1;
が設定されている点です。
+ if(n->left->type->width == widthptr && + isint[simsimtype(n->left->type)]) { + /* For pointer types, we can make a special form of optimization + * + * These statements are put onto the expression init list: + * Itab *tab = atomicloadtype(&cache); + * if(tab == nil) + * tab = typ2Itab(type, itype, &cache); + * + * The CONVIFACE expression is replaced with this: + * OEFACE{tab, ptr}; + */ + l = temp(ptrto(types[TUINT8])); + + n1 = nod(OAS, l, sym->def); + typecheck(&n1, Etop); + *init = list(*init, n1); + + fn = syslook("typ2Itab", 1); + n1 = nod(OCALL, fn, N); + n1->list = ll; + typecheck(&n1, Erv); + walkexpr(&n1, init); + + n2 = nod(OIF, N, N); + n2->ntest = nod(OEQ, l, nodnil()); + n2->nbody = list1(nod(OAS, l, n1)); + n2->likely = -1; // Mark this slow path as unlikely + typecheck(&n2, Etop); + *init = list(*init, n2); + + l = nod(OEFACE, l, n->left); + l->typecheck = n->typecheck; + l->type = n->type; + n = l; + goto ret; + }
-
src/pkg/runtime/iface.c
:- 新しいランタイム関数
runtime.typ2Itab
の実装が追加されています。この関数は、指定された型とインターフェース型に対応するItab
を検索または生成し、キャッシュに格納します。
+#pragma textflag 7 +void +runtime·typ2Itab(Type *t, InterfaceType *inter, Itab **cache, Itab *ret) +{ + Itab *tab; + + tab = itab(inter, t, 0); + runtime·atomicstorep(cache, tab); + ret = tab; + FLUSH(&ret); +}
- 新しいランタイム関数
コアとなるコードの解説
src/cmd/gc/builtin.c
と src/cmd/gc/runtime.go
の変更
これらのファイルは、Goコンパイラが認識する組み込み関数やランタイム関数の定義に関連しています。builtin.c
では、コンパイラがC言語で書かれたランタイム関数を呼び出すための「外部関数」としてtyp2Itab
のシグネチャが追加されています。一方、runtime.go
では、Go言語側からこのランタイム関数を呼び出すためのGoの関数シグネチャが追加されています。これにより、コンパイラはGoコード内でtyp2Itab
を呼び出す際に、その型情報を正しく解決できるようになります。
src/cmd/gc/go.h
の変更
Node
構造体へのschar likely;
の追加は、ASTの各ノード、特にif
文のような条件分岐を表すノードに、その条件が真になる可能性のヒントを付与できるようにするための基盤となります。schar
は符号付き文字型であり、-1
(unlikely)、0
(不明)、1
(likely)などの値を格納できます。この情報は、後続のコード生成フェーズで利用され、より効率的な分岐命令の選択やコード配置に役立ちます。
src/cmd/gc/gen.c
の変更
gen.c
は、ASTから機械語コードを生成する部分を担当しています。bgen
関数は、条件分岐(if
文など)のコードを生成する際に呼び出されます。変更点であるbgen(n->ntest, 0, -n->likely, p2);
は、n->likely
フィールドの値を考慮して分岐命令を生成することを示しています。例えば、n->likely
が-1
(unlikely)の場合、-n->likely
は1
となり、コンパイラはこの分岐が真になる可能性が低いと判断し、その情報に基づいて最適化を行います。これにより、CPUの分岐予測器がより正確に予測できるようになり、パイプラインストールを減らすことでパフォーマンスが向上します。
src/cmd/gc/walk.c
の変更
walk.c
は、ASTを走査し、最適化やコード変換を行う「ウォーカー」の役割を担っています。このファイルへの変更は、今回のコミットの核心部分です。
追加されたコードブロックは、ポインタサイズの型からインターフェースへの変換(CONVIFACE
オペレーション)を検出した際に、特別な最適化パスを適用します。
- キャッシュ参照: まず、
Itab
キャッシュ(sym->def
)を直接参照し、対応するItab
が既にキャッシュされているかどうかを確認します。 - キャッシュミス時の処理:
- もしキャッシュが
nil
(キャッシュミス)であれば、runtime.typ2Itab
関数を呼び出します。この関数は、ランタイムでItab
を検索または生成し、キャッシュに格納します。 - このキャッシュミスをチェックする
if
文(n2 = nod(OIF, N, N); n2->ntest = nod(OEQ, l, nodnil());
)に対して、n2->likely = -1;
が設定されています。これは、キャッシュミスが稀にしか発生しない「遅いパス」であることをコンパイラに明示的に伝えます。これにより、コンパイラはキャッシュヒットの「速いパス」を優先的に最適化し、キャッシュミス時のコードはあまり最適化しないようにします。
- もしキャッシュが
- インライン化されたインターフェース値の構築: キャッシュされた
Itab
(またはruntime.typ2Itab
によって取得されたItab
)と、元のポインタサイズの値を組み合わせて、新しいインターフェース値(OEFACE
)を直接構築します。これにより、従来のランタイム関数呼び出しによるオーバーヘッドが排除されます。
この一連の処理により、ポインタサイズのT2I変換は、ほとんどの場合、関数呼び出しなしでインラインで処理されるようになり、大幅なパフォーマンス向上が実現されます。
src/pkg/runtime/iface.c
の変更
このファイルはGoランタイムのインターフェース関連の処理を実装しています。追加されたruntime.typ2Itab
関数は、コンパイラがインライン化されたT2I変換の「遅いパス」から呼び出すための実体です。
itab(inter, t, 0)
: この関数は、指定されたインターフェース型inter
と具体的な型t
に対応するItab
を検索または生成します。0
は、Itab
が見つからない場合にパニックしないことを意味します。runtime·atomicstorep(cache, tab)
: 取得したItab
を、コンパイラが渡してきたキャッシュポインタcache
にアトミックに格納します。これにより、複数のゴルーチンが同時にItab
を検索/生成しようとした場合でも、キャッシュの一貫性が保たれます。ret = tab; FLUSH(&ret);
: 取得したItab
を戻り値として設定し、キャッシュをフラッシュします。
このランタイム関数は、インライン化されたコードがキャッシュミスした場合のフォールバックメカニズムとして機能し、必要に応じてItab
を効率的に取得・キャッシュします。
関連リンク
- Go言語のインターフェースに関する公式ドキュメント: https://go.dev/tour/methods/9
- Goのインターフェースの内部表現に関するブログ記事(非公式ですが参考になります):
- The Laws of Reflection: https://go.dev/blog/laws-of-reflection
- Go Data Structures: Interfaces: https://research.swtch.com/interfaces
- Goコンパイラの内部構造に関する情報(古いものも含まれますが、概念理解に役立ちます):
- Go Compiler Internals: https://go.dev/doc/articles/go-compiler-internals
参考にした情報源リンク
- コミットハッシュ: 8fd65b0e1d6b737072982a1cf846ff76b1353f02
- GitHub上のコミットページ: https://github.com/golang/go/commit/8fd65b0e1d6b737072982a1cf846ff76b1353f02
- Goのインターフェースの内部構造に関する一般的な知識
- コンパイラ最適化(インライン化、分岐予測)に関する一般的な知識
- Go言語のランタイムに関する一般的な知識
- Goの
Itab
に関する情報(非公式のブログ記事やGoのソースコード解析) - Goのベンチマーク結果の解釈に関する一般的な知識