Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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 が、キャッシュが良好で Tuintptr 型の場合に、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言語の内部動作に関する知識が不可欠です。

  1. Goのインターフェース: Goのインターフェースは、メソッドのシグネチャの集合を定義する型です。Goのインターフェースは、他の言語のインターフェースとは異なり、明示的な実装宣言を必要としません。ある具象型がインターフェースで定義されたすべてのメソッドを実装していれば、その具象型はそのインターフェースを「実装している」とみなされます(構造的型付け)。 Goのインターフェース値は、内部的に2つのポインタで構成されています。

    • 型情報ポインタ (Type pointer): インターフェース値が保持している具象型の型情報 (_type 構造体) へのポインタ。
    • データポインタ (Data pointer): インターフェース値が保持している具象型のインスタンスデータへのポインタ。
  2. itab (Interface Table): インターフェース値が具象型を保持している場合、その具象型がインターフェースのメソッドをどのように実装しているかという情報が必要です。この情報が itab に格納されています。itab は、特定の具象型 (_type) と特定のインターフェース型 (InterfaceType) のペアに対して一意に生成されます。 itab の主な役割は以下の通りです。

    • 具象型がインターフェースを実装しているかどうかの確認。
    • インターフェースのメソッドが呼び出された際に、対応する具象型のメソッドの実装を見つけるためのディスパッチテーブル(メソッドポインタの配列)。 itab は、Goプログラムの実行中に必要に応じて動的に生成され、キャッシュされます。
  3. convT2IconvT2E: これらはGoランタイムの内部関数で、それぞれ具象型からインターフェース型への変換を担当します。

    • convT2I (Convert Type to Interface): 具象型を特定の非空インターフェース型(例: io.Reader)に変換する際に呼び出されます。この関数は、具象型とインターフェース型の両方を受け取り、対応する itab を見つける必要があります。
    • convT2E (Convert Type to Empty Interface): 具象型を空インターフェース型 (interface{}) に変換する際に呼び出されます。空インターフェースはメソッドを持たないため、itab は不要です。この変換は convT2I よりも軽量です。
  4. uintptr-shaped: Goの内部では、特定の型の値が uintptr (ポインタを保持できる整数型) のサイズに収まる場合、その型は「uintptr-shaped」であると表現されることがあります。これは、インターフェースのデータポインタに直接値を格納できることを意味し、別途ヒープアロケーションを必要としないため、パフォーマンス上有利です。

  5. アトミック操作 (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 関数内部では、以下のロジックが追加されました。

  1. まず、runtime·atomicloadp(cache) を使用して、キャッシュポインタが指す場所から既存の itab をアトミックに読み込もうとします。
  2. もしキャッシュされた itab が存在すれば (!tab が偽であれば)、そのキャッシュされた itab を直接使用します。これにより、itab 関数の呼び出しとそれに伴うルックアップ処理がスキップされます。
  3. もしキャッシュされた itab が存在しない場合 (!tab が真であれば)、従来通り itab(inter, t, 0) を呼び出して新しい itab を取得します。
  4. 取得した itab は、runtime·atomicstorep(cache, tab) を使用して、キャッシュポインタが指す場所にアトミックに書き込まれます。これにより、次回同じ変換が行われた際に、この itab が再利用されるようになります。

このキャッシュメカニズムにより、一度 itab がルックアップされれば、その後の同じ具象型から同じインターフェース型への変換は、非常に高速なキャッシュヒットパスを通るようになります。これは、特にホットパス(頻繁に実行されるコードパス)でのインターフェース変換のパフォーマンスを劇的に改善します。

また、コンパイラ側 (src/cmd/gc/walk.c) では、convT2I の呼び出しを生成する際に、この新しいキャッシュ引数を渡すためのコードが追加されました。具体的には、itabpkg という新しい「偽のパッケージ」が導入され、itab キャッシュのためのシンボルが生成されるようになりました。これにより、コンパイラは各 (具象型, インターフェース型) ペアに対して、対応する itab キャッシュ変数を生成し、それを convT2I に渡すことができるようになります。

コミットメッセージにあるように、BenchmarkConvT2ISmallBenchmarkConvT2IUintptrBenchmarkConvT2ILarge のベンチマーク結果は、それぞれ64.01%、67.48%、22.31%という大幅な性能向上を示しており、この最適化が非常に効果的であったことを裏付けています。

コアとなるコードの変更箇所

このコミットにおける主要なコード変更は以下のファイルに集中しています。

  1. src/pkg/runtime/iface.c:

    • runtime·convT2I 関数のシグネチャが変更され、cache **byte という新しい引数が追加されました。
    • 関数内部で、cache 引数を利用して itab のアトミックな読み込み (runtime·atomicloadp) と書き込み (runtime·atomicstorep) が行われるようになりました。これにより、itab のキャッシュロジックが実装されています。
  2. src/cmd/gc/walk.c:

    • コンパイラのウォークフェーズにおいて、convT2I の呼び出しを生成する際に、itab キャッシュのためのシンボル (sym) を生成し、そのアドレスを convT2I の新しい引数として渡すロジックが追加されました。
    • itabpkg という新しいパッケージが導入され、itab キャッシュ変数の名前解決に使用されます。
  3. src/cmd/gc/builtin.c および src/cmd/gc/runtime.go:

    • convT2I の関数シグネチャが、新しい cache 引数を含むように更新されました。これらはコンパイラがランタイム関数を認識するための定義ファイルです。
  4. src/cmd/gc/go.h および src/cmd/gc/lex.c:

    • itabpkg という新しい偽のパッケージが宣言され、初期化されるようになりました。これはコンパイラが itab キャッシュ変数を管理するために使用します。
  5. 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 関数の中心的なロジックです。

  1. runtime·convT2I の引数に Itab **cache が追加されました。これは、itab ポインタを格納するメモリ位置へのポインタです。
  2. elem の計算が &inter+1 から &cache+1 に変更されています。これは、新しい引数 cache が追加されたため、スタック上の引数のオフセットがずれたことによる調整です。
  3. 最も重要な変更は、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 が再利用されます。
    • キャッシュヒットの場合(tabnil でない場合)、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 (具象型からインターフェース型への変換) ノードを処理する際に実行されます。

  1. if(!isinter(n->left->type) && !isnilinter(n->type)) は、変換元が具象型であり、変換先が空ではないインターフェース型である場合にのみ、このキャッシュロジックを適用することを示しています。
  2. sym = pkglookup(smprint("%-T.%-T", n->left->type, n->type), itabpkg); は、変換元の具象型と変換先のインターフェース型の名前を組み合わせた文字列を基に、itabpkg という新しい「偽のパッケージ」内でシンボルをルックアップまたは生成します。このシンボルが itab キャッシュ変数を表します。
  3. 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 フラグが追加されているため、最終的には読み取り専用データセクションに配置される可能性があります)。
  4. l = nod(OADDR, sym->def, N); は、生成された itab キャッシュ変数のアドレスを取得します。
  5. ll = list(ll, l); は、このアドレスを convT2I 関数呼び出しの引数リストに追加します。これにより、ランタイムの convT2I 関数は、このキャッシュ変数のアドレスを受け取り、itab のキャッシュと再利用を行うことができるようになります。

これらの変更により、コンパイラは各具象型からインターフェース型への変換サイトに対して、専用の itab キャッシュ変数を生成し、ランタイムがそのキャッシュを利用してパフォーマンスを向上させるための基盤が構築されました。

関連リンク

参考にした情報源リンク

  • 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.gotest/bench/go1 ディレクトリにベンチマークテストが含まれています。
  • Go言語の sync/atomic パッケージに関するドキュメント (アトミック操作について): https://pkg.go.dev/sync/atomic
  • Go言語のコンパイラとランタイムの内部に関する一般的な知識。