[インデックス 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言語のコンパイラとランタイムの内部に関する一般的な知識。