[インデックス 13435] ファイルの概要
このコミットは、Go言語のコンパイラ (cmd/gc) とランタイム (src/pkg/runtime) における重要なパフォーマンス最適化を導入しています。具体的には、具象型からインターフェース型への変換 (convT2I 関数) において、itab (インターフェーステーブル) のルックアップ結果をキャッシュするメカニズムを追加することで、この変換処理のオーバーヘッドを大幅に削減しています。これにより、特にインターフェース変換が頻繁に行われるアプリケーションにおいて、実行速度の向上が期待されます。
コミット
commit 18e86644a3ca65c6283018d1da7c6c08f1fe9454
Author: Nigel Tao <nigeltao@golang.org>
Date: Tue Jul 3 09:09:05 2012 +1000
cmd/gc: cache itab lookup in convT2I.
There may be further savings if convT2I can avoid the function call
if the cache is good and T is uintptr-shaped, a la convT2E, but that
will be a follow-up CL.
src/pkg/runtime:
benchmark old ns/op new ns/op delta
BenchmarkConvT2ISmall 43 15 -64.01%
BenchmarkConvT2IUintptr 45 14 -67.48%
BenchmarkConvT2ILarge 130 101 -22.31%
test/bench/go1:
benchmark old ns/op new ns/op delta
BenchmarkBinaryTree17 8588997000 8499058000 -1.05%
BenchmarkFannkuch11 5300392000 5358093000 +1.09%
BenchmarkGobDecode 30295580 31040190 +2.46%
BenchmarkGobEncode 18102070 17675650 -2.36%
BenchmarkGzip 774191400 771591400 -0.34%
BenchmarkGunzip 245915100 247464100 +0.63%
BenchmarkJSONEncode 123577000 121423050 -1.74%
BenchmarkJSONDecode 451969800 596256200 +31.92%
BenchmarkMandelbrot200 10060050 10072880 +0.13%
BenchmarkParse 10989840 11037710 +0.44%
BenchmarkRevcomp 1782666000 1716864000 -3.69%
BenchmarkTemplate 798286600 723234400 -9.40%
R=rsc, bradfitz, go.peter.90, daniel.morsing, dave, uriel
CC=golang-dev
https://golang.org/cl/6337058
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/18e86644a3ca65c6283018d1da7c6c08f1fe9454
元コミット内容
cmd/gc: cache itab lookup in convT2I.
convT2I における itab ルックアップをキャッシュします。
convT2I が、キャッシュが良好で T が uintptr 型の場合に、convT2E のように関数呼び出しを回避できれば、さらなる節約が可能かもしれませんが、それは今後の変更リスト (CL) で対応します。
src/pkg/runtime のベンチマーク結果:
BenchmarkConvT2ISmall: 43 ns/op -> 15 ns/op (-64.01%)BenchmarkConvT2IUintptr: 45 ns/op -> 14 ns/op (-67.48%)BenchmarkConvT2ILarge: 130 ns/op -> 101 ns/op (-22.31%)
test/bench/go1 のベンチマーク結果:
BenchmarkBinaryTree17: 8588997000 ns/op -> 8499058000 ns/op (-1.05%)BenchmarkFannkuch11: 5300392000 ns/op -> 5358093000 ns/op (+1.09%)BenchmarkGobDecode: 30295580 ns/op -> 31040190 ns/op (+2.46%)BenchmarkGobEncode: 18102070 ns/op -> 17675650 ns/op (-2.36%)BenchmarkGzip: 774191400 ns/op -> 771591400 ns/op (-0.34%)BenchmarkGunzip: 245915100 ns/op -> 247464100 ns/op (+0.63%)BenchmarkJSONEncode: 123577000 ns/op -> 121423050 ns/op (-1.74%)BenchmarkJSONDecode: 451969800 ns/op -> 596256200 ns/op (+31.92%)BenchmarkMandelbrot200: 10060050 ns/op -> 10072880 ns/op (+0.13%)BenchmarkParse: 10989840 ns/op -> 11037710 ns/op (+0.44%)BenchmarkRevcomp: 1782666000 ns/op -> 1716864000 ns/op (-3.69%)BenchmarkTemplate: 798286600 ns/op -> 723234400 ns/op (-9.40%)
変更の背景
Go言語において、具象型からインターフェース型への変換は頻繁に行われる操作です。この変換の際、Goランタイムは、具象型が特定のインターフェースを実装しているかどうかを判断し、そのインターフェースのメソッドセットへのポインタを含む itab (interface table) と呼ばれる構造体を見つける必要があります。
従来の convT2I (Convert Type to Interface) 関数は、この itab のルックアップを毎回実行していました。しかし、同じ具象型が同じインターフェース型に繰り返し変換される場合、このルックアップは冗長なオーバーヘッドとなります。特に、ループ内でインターフェース変換が行われるようなシナリオでは、このオーバーヘッドが顕著になり、アプリケーション全体のパフォーマンスに影響を与える可能性がありました。
このコミットの目的は、この冗長な itab ルックアップを排除し、パフォーマンスを向上させることです。具体的には、一度ルックアップした itab をキャッシュすることで、後続の同じ変換ではキャッシュされた値を利用できるようにします。これにより、convT2I の実行時間を大幅に短縮し、Goプログラムの全体的な実行効率を高めることが狙いです。コミットメッセージに示されているベンチマーク結果は、この最適化が特に convT2I 関連の操作において劇的な改善をもたらしたことを明確に示しています。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGo言語の内部動作に関する知識が不可欠です。
-
Goのインターフェース: Goのインターフェースは、メソッドのシグネチャの集合を定義する型です。Goのインターフェースは、他の言語のインターフェースとは異なり、明示的な実装宣言を必要としません。ある具象型がインターフェースで定義されたすべてのメソッドを実装していれば、その具象型はそのインターフェースを「実装している」とみなされます(構造的型付け)。 Goのインターフェース値は、内部的に2つのポインタで構成されています。
- 型情報ポインタ (Type pointer): インターフェース値が保持している具象型の型情報 (
_type構造体) へのポインタ。 - データポインタ (Data pointer): インターフェース値が保持している具象型のインスタンスデータへのポインタ。
- 型情報ポインタ (Type pointer): インターフェース値が保持している具象型の型情報 (
-
itab(Interface Table): インターフェース値が具象型を保持している場合、その具象型がインターフェースのメソッドをどのように実装しているかという情報が必要です。この情報がitabに格納されています。itabは、特定の具象型 (_type) と特定のインターフェース型 (InterfaceType) のペアに対して一意に生成されます。itabの主な役割は以下の通りです。- 具象型がインターフェースを実装しているかどうかの確認。
- インターフェースのメソッドが呼び出された際に、対応する具象型のメソッドの実装を見つけるためのディスパッチテーブル(メソッドポインタの配列)。
itabは、Goプログラムの実行中に必要に応じて動的に生成され、キャッシュされます。
-
convT2IとconvT2E: これらはGoランタイムの内部関数で、それぞれ具象型からインターフェース型への変換を担当します。convT2I(Convert Type to Interface): 具象型を特定の非空インターフェース型(例:io.Reader)に変換する際に呼び出されます。この関数は、具象型とインターフェース型の両方を受け取り、対応するitabを見つける必要があります。convT2E(Convert Type to Empty Interface): 具象型を空インターフェース型 (interface{}) に変換する際に呼び出されます。空インターフェースはメソッドを持たないため、itabは不要です。この変換はconvT2Iよりも軽量です。
-
uintptr-shaped: Goの内部では、特定の型の値がuintptr(ポインタを保持できる整数型) のサイズに収まる場合、その型は「uintptr-shaped」であると表現されることがあります。これは、インターフェースのデータポインタに直接値を格納できることを意味し、別途ヒープアロケーションを必要としないため、パフォーマンス上有利です。 -
アトミック操作 (
atomicloadp,atomicstorep): マルチスレッド環境において、共有データ(この場合はitabキャッシュ)へのアクセスを安全に行うために使用される操作です。atomicloadp: ポインタの値をアトミックに読み込みます。他のゴルーチンによる書き込みと競合することなく、常に最新の値を読み込むことを保証します。atomicstorep: ポインタの値をアトミックに書き込みます。他のゴルーチンによる読み書きと競合することなく、安全に値を更新します。itabキャッシュは複数のゴルーチンからアクセスされる可能性があるため、データ競合を防ぎ、キャッシュの一貫性を保つためにアトミック操作が不可欠です。
技術的詳細
このコミットの核心は、convT2I 関数が itab をルックアップする際に、その結果をキャッシュするメカニズムを導入した点にあります。
従来の convT2I は、具象型 T とインターフェース型 I のペアが与えられるたびに、itab(I, T, 0) というランタイム関数を呼び出して、対応する itab を取得していました。この itab 関数は、内部的にハッシュテーブルのようなデータ構造を検索し、必要であれば新しい itab を構築します。この検索と構築のプロセスは、特に頻繁に実行される場合にパフォーマンスのボトルネックとなる可能性がありました。
このコミットでは、convT2I のシグネチャに新しい引数 cache **byte が追加されました。この cache 引数は、itab へのポインタを格納するためのポインタです。コンパイラは、convT2I を呼び出す際に、このキャッシュポインタを渡すように変更されます。
runtime·convT2I 関数内部では、以下のロジックが追加されました。
- まず、
runtime·atomicloadp(cache)を使用して、キャッシュポインタが指す場所から既存のitabをアトミックに読み込もうとします。 - もしキャッシュされた
itabが存在すれば (!tabが偽であれば)、そのキャッシュされたitabを直接使用します。これにより、itab関数の呼び出しとそれに伴うルックアップ処理がスキップされます。 - もしキャッシュされた
itabが存在しない場合 (!tabが真であれば)、従来通りitab(inter, t, 0)を呼び出して新しいitabを取得します。 - 取得した
itabは、runtime·atomicstorep(cache, tab)を使用して、キャッシュポインタが指す場所にアトミックに書き込まれます。これにより、次回同じ変換が行われた際に、このitabが再利用されるようになります。
このキャッシュメカニズムにより、一度 itab がルックアップされれば、その後の同じ具象型から同じインターフェース型への変換は、非常に高速なキャッシュヒットパスを通るようになります。これは、特にホットパス(頻繁に実行されるコードパス)でのインターフェース変換のパフォーマンスを劇的に改善します。
また、コンパイラ側 (src/cmd/gc/walk.c) では、convT2I の呼び出しを生成する際に、この新しいキャッシュ引数を渡すためのコードが追加されました。具体的には、itabpkg という新しい「偽のパッケージ」が導入され、itab キャッシュのためのシンボルが生成されるようになりました。これにより、コンパイラは各 (具象型, インターフェース型) ペアに対して、対応する itab キャッシュ変数を生成し、それを convT2I に渡すことができるようになります。
コミットメッセージにあるように、BenchmarkConvT2ISmall、BenchmarkConvT2IUintptr、BenchmarkConvT2ILarge のベンチマーク結果は、それぞれ64.01%、67.48%、22.31%という大幅な性能向上を示しており、この最適化が非常に効果的であったことを裏付けています。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下のファイルに集中しています。
-
src/pkg/runtime/iface.c:runtime·convT2I関数のシグネチャが変更され、cache **byteという新しい引数が追加されました。- 関数内部で、
cache引数を利用してitabのアトミックな読み込み (runtime·atomicloadp) と書き込み (runtime·atomicstorep) が行われるようになりました。これにより、itabのキャッシュロジックが実装されています。
-
src/cmd/gc/walk.c:- コンパイラのウォークフェーズにおいて、
convT2Iの呼び出しを生成する際に、itabキャッシュのためのシンボル (sym) を生成し、そのアドレスをconvT2Iの新しい引数として渡すロジックが追加されました。 itabpkgという新しいパッケージが導入され、itabキャッシュ変数の名前解決に使用されます。
- コンパイラのウォークフェーズにおいて、
-
src/cmd/gc/builtin.cおよびsrc/cmd/gc/runtime.go:convT2Iの関数シグネチャが、新しいcache引数を含むように更新されました。これらはコンパイラがランタイム関数を認識するための定義ファイルです。
-
src/cmd/gc/go.hおよびsrc/cmd/gc/lex.c:itabpkgという新しい偽のパッケージが宣言され、初期化されるようになりました。これはコンパイラがitabキャッシュ変数を管理するために使用します。
-
src/pkg/runtime/iface_test.goおよびtest/{convT2E.go => convT2X.go}:convT2Iのパフォーマンスを測定するための新しいベンチマークが追加され、既存のテストファイルがリファクタリングされました。これにより、最適化の効果が検証されています。
その他、src/cmd/{5g,6g,8g}/gsubr.c, src/cmd/gc/obj.c, src/cmd/gc/reflect.c など、コンパイラのバックエンドやリフレクション関連のファイルでも、ggloblsym 関数のシグネチャ変更(rodata 引数の追加)や、文字列リテラル、型情報などのグローバルシンボル定義に関する微調整が行われています。これは、itab キャッシュ変数が読み取り専用データセクションに配置されることに関連している可能性があります。
コアとなるコードの解説
src/pkg/runtime/iface.c の変更
// func convT2I(typ *byte, typ2 *byte, cache **byte, elem any) (ret any)
#pragma textflag 7
void
runtime·convT2I(Type *t, InterfaceType *inter, Itab **cache, ...)
{
byte *elem;
Iface *ret;
Itab *tab; // Itab ポインタを格納する変数
int32 wid;
- elem = (byte*)(&inter+1);
+ elem = (byte*)(&cache+1); // elem のオフセットが変更された
wid = t->size;
ret = (Iface*)(elem + ROUND(wid, Structrnd));
- ret->tab = itab(inter, t, 0); // 毎回 itab をルックアップしていた
+ tab = runtime·atomicloadp(cache); // キャッシュから itab をアトミックに読み込む
+ if(!tab) { // キャッシュミスの場合
+ tab = itab(inter, t, 0); // itab をルックアップ
+ runtime·atomicstorep(cache, tab); // 取得した itab をキャッシュにアトミックに書き込む
+ }
+ ret->tab = tab; // キャッシュされた、または新しく取得した itab を使用
copyin(t, elem, &ret->data);
}
この変更は convT2I 関数の中心的なロジックです。
runtime·convT2Iの引数にItab **cacheが追加されました。これは、itabポインタを格納するメモリ位置へのポインタです。elemの計算が&inter+1から&cache+1に変更されています。これは、新しい引数cacheが追加されたため、スタック上の引数のオフセットがずれたことによる調整です。- 最も重要な変更は、
itab(inter, t, 0)の呼び出しの前にキャッシュチェックが追加されたことです。tab = runtime·atomicloadp(cache);は、cacheが指すメモリ位置からitabポインタをアトミックに読み込みます。これにより、複数のゴルーチンが同時にこのキャッシュにアクセスしても、データ競合が発生しないことが保証されます。if(!tab)は、キャッシュが空(nil)であるかどうかをチェックします。- キャッシュが空の場合、
tab = itab(inter, t, 0);を呼び出して、従来のitabルックアップを実行します。 - ルックアップで
itabが取得された後、runtime·atomicstorep(cache, tab);を使用して、取得したitabをアトミックにキャッシュに書き込みます。これにより、次回同じ変換が行われた際に、このitabが再利用されます。 - キャッシュヒットの場合(
tabがnilでない場合)、itabルックアップはスキップされ、キャッシュされたtabが直接使用されます。
src/cmd/gc/walk.c の変更
// walkexpr 関数内の一部
// ...
case OCONVIFACE: // 具象型からインターフェース型への変換
// ...
if(!isinter(n->left->type) && !isnilinter(n->type)){ // 具象型から非空インターフェースへの変換の場合
sym = pkglookup(smprint("%-T.%-T", n->left->type, n->type), itabpkg); // itabpkg を使ってキャッシュシンボル名を生成
if(sym->def == N) { // シンボルが未定義の場合
l = nod(ONAME, N, N);
l->sym = sym;
l->type = ptrto(types[TUINT8]); // ポインタ型
l->addable = 1;
l->class = PEXTERN; // 外部シンボルとしてマーク
l->xoffset = 0;
sym->def = l;
ggloblsym(sym, widthptr, 1, 0); // グローバルシンボルとして定義
}
l = nod(OADDR, sym->def, N); // シンボルのアドレスを取得
l->addable = 1;
ll = list(ll, l); // convT2I の引数リストに追加
}
ll = list(ll, n->left); // 変換元の具象型
argtype(fn, n->left->type);
argtype(fn, n->type);
// ...
このコードは、コンパイラが OCONVIFACE (具象型からインターフェース型への変換) ノードを処理する際に実行されます。
if(!isinter(n->left->type) && !isnilinter(n->type))は、変換元が具象型であり、変換先が空ではないインターフェース型である場合にのみ、このキャッシュロジックを適用することを示しています。sym = pkglookup(smprint("%-T.%-T", n->left->type, n->type), itabpkg);は、変換元の具象型と変換先のインターフェース型の名前を組み合わせた文字列を基に、itabpkgという新しい「偽のパッケージ」内でシンボルをルックアップまたは生成します。このシンボルがitabキャッシュ変数を表します。if(sym->def == N)ブロックは、この(具象型, インターフェース型)ペアに対するitabキャッシュ変数がまだ定義されていない場合に実行されます。nod(ONAME, N, N)で新しいノードを作成し、そのシンボルをsymに設定します。l->type = ptrto(types[TUINT8]);は、このキャッシュ変数がitabポインタを格納するためのポインタ型であることを示します。l->class = PEXTERN;は、この変数が外部リンケージを持つグローバル変数であることを示します。ggloblsym(sym, widthptr, 1, 0);は、このシンボルをグローバル変数として定義します。widthptrはポインタのサイズ、1は重複を許可すること、0は読み取り専用データではないことを示します(ただし、他の変更でrodataフラグが追加されているため、最終的には読み取り専用データセクションに配置される可能性があります)。
l = nod(OADDR, sym->def, N);は、生成されたitabキャッシュ変数のアドレスを取得します。ll = list(ll, l);は、このアドレスをconvT2I関数呼び出しの引数リストに追加します。これにより、ランタイムのconvT2I関数は、このキャッシュ変数のアドレスを受け取り、itabのキャッシュと再利用を行うことができるようになります。
これらの変更により、コンパイラは各具象型からインターフェース型への変換サイトに対して、専用の itab キャッシュ変数を生成し、ランタイムがそのキャッシュを利用してパフォーマンスを向上させるための基盤が構築されました。
関連リンク
- Go言語のインターフェースに関する公式ドキュメント: https://go.dev/tour/methods/10
- Go言語の内部構造に関するブログ記事 (itabなど):
- The Laws of Reflection: https://go.dev/blog/laws-of-reflection
- Go Data Structures: Interfaces: https://research.swtch.com/interfaces
- Go言語のコンパイラとランタイムのソースコード (GoのGitHubリポジトリ): https://github.com/golang/go
参考にした情報源リンク
- Go言語のコミット履歴: https://github.com/golang/go/commits/master
- Go言語のコードレビューシステム (Gerrit): https://go.dev/cl/ (コミットメッセージに記載されている
https://golang.org/cl/6337058は、このGerritシステムへのリンクです。) - Go言語のベンチマーク結果: Goのソースコード内の
src/pkg/runtime/iface_test.goやtest/bench/go1ディレクトリにベンチマークテストが含まれています。 - Go言語の
sync/atomicパッケージに関するドキュメント (アトミック操作について): https://pkg.go.dev/sync/atomic - Go言語のコンパイラとランタイムの内部に関する一般的な知識。