[インデックス 13351] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)において、特定の条件下での型変換(convT2E
)のインライン化を導入するものです。具体的には、uintptr
型に似た(uintptr-shaped
)型から空のインターフェース(interface{}
)への変換を最適化し、パフォーマンスを向上させます。
コミット
commit 8f84328fdc6f625359c8fc8498cfc48689673ea3
Author: Nigel Tao <nigeltao@golang.org>
Date: Thu Jun 14 10:43:20 2012 +1000
cmd/gc: inline convT2E when T is uintptr-shaped.
GOARCH=amd64 benchmarks
src/pkg/runtime
benchmark old ns/op new ns/op delta
BenchmarkConvT2ESmall 10 10 +1.00%
BenchmarkConvT2EUintptr 9 0 -92.07%
BenchmarkConvT2EBig 74 74 -0.27%
BenchmarkConvT2I 27 26 -3.62%
BenchmarkConvI2E 4 4 -7.05%
BenchmarkConvI2I 20 19 -2.99%
test/bench/go1
benchmark old ns/op new ns/op delta
BenchmarkBinaryTree17 5930908000 5937260000 +0.11%
BenchmarkFannkuch11 3927057000 3933556000 +0.17%
BenchmarkGobDecode 21998090 21870620 -0.58%
BenchmarkGobEncode 12725310 12734480 +0.07%
BenchmarkGzip 567617600 567892800 +0.05%
BenchmarkGunzip 178284100 178706900 +0.24%
BenchmarkJSONEncode 87693550 86794300 -1.03%
BenchmarkJSONDecode 314212600 324115000 +3.15%
BenchmarkMandelbrot200 7016640 7073766 +0.81%
BenchmarkParse 7852100 7892085 +0.51%
BenchmarkRevcomp 1285663000 1286147000 +0.04%
BenchmarkTemplate 566823800 567606200 +0.14%
I'm not entirely sure why the JSON* numbers have changed, but
eyeballing the profile suggests that it could be spending less
and more time in runtime.{new,old}stack, so it could simply be
stack-split boundary noise.
R=rsc, dave, bsiegert, dsymonds
CC=golang-dev
https://golang.org/cl/6280049
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8f84328fdc6f625359c8fc8498cfc48689673ea3
元コミット内容
このコミットは、Goコンパイラが非インターフェース型から空のインターフェース型への変換(convT2E
)を処理する方法を改善します。特に、uintptr
型と同じサイズとアラインメントを持つ型(uintptr-shaped
)の場合に、この変換をより効率的にインライン化するように変更します。これにより、BenchmarkConvT2EUintptr
のベンチマークで92%以上の大幅なパフォーマンス向上が見られます。
変更の背景
Go言語では、型アサーションやインターフェースへの型変換が頻繁に行われます。特に、具体的な型から空のインターフェース(interface{}
)への変換は、Goの柔軟な型システムを支える重要な操作です。しかし、これらの変換は実行時に型情報や値のコピーを伴うため、オーバーヘッドが発生する可能性があります。
このコミットの背景には、このような型変換のパフォーマンスボトルネックを解消し、Goプログラム全体の実行速度を向上させるという目的があります。特に、uintptr
のようなポインタサイズにフィットする型は、インターフェースへの変換時に特別な最適化の機会を提供します。これは、空のインターフェースが通常、型情報と値の2つのワードで構成されるため、uintptr
のような1ワードで表現できる型の場合、値のコピーをより直接的に行える可能性があるためです。
前提知識の解説
Goのインターフェース
Goのインターフェースは、メソッドの集合を定義する型です。Goのインターフェースには大きく分けて2種類あります。
- 非空インターフェース (Non-empty Interface): 少なくとも1つのメソッドを持つインターフェースです。例えば
io.Reader
など。 - 空インターフェース (Empty Interface): メソッドを一切持たないインターフェースで、
interface{}
と表記されます。Goのあらゆる型の値を格納できるため、非常に汎用的に使われます。
Goのインターフェース値は、内部的には2つのワードで構成されます(通常、ポインタサイズと同じ)。
- 型情報 (Type Word): 格納されている具体的な値の型を記述するポインタ(
_type
構造体へのポインタ)。 - 値 (Value Word): 格納されている具体的な値そのもの、またはその値へのポインタ。
例えば、int
型の値が interface{}
に変換される場合、インターフェース値は int
型の型情報と、int
値そのもの(またはそのコピー)を保持します。
convT2E
convT2E
は "convert Type to Empty interface" の略で、Goコンパイラが具体的な型(T
)から空のインターフェース(E
、つまりinterface{}
)への変換を処理する内部的な操作を指します。この操作は、コンパイル時に生成されるコードの一部であり、実行時にインターフェース値の構築を行います。
uintptr-shaped
型
uintptr-shaped
とは、その型がuintptr
型と同じサイズ(通常はポインタサイズ、32ビットまたは64ビット)とアラインメントを持つことを意味します。例えば、int
、uint
、unsafe.Pointer
、ポインタ型(*T
)などがこれに該当します。これらの型は、メモリ上で1ワードで表現できるため、インターフェースへの変換時に特別な最適化の対象となりやすいです。
Goコンパイラのバックエンド (cmd/gc
, cmd/5g
, cmd/6g
, cmd/8g
)
Goコンパイラは、ソースコードを中間表現に変換し、最終的に各アーキテクチャ向けの機械語を生成します。
cmd/gc
: Goコンパイラの主要部分で、プラットフォーム非依存の最適化やコード生成を行います。cmd/5g
: ARMアーキテクチャ向けのコード生成バックエンド。cmd/6g
: AMD64アーキテクチャ向けのコード生成バックエンド。cmd/8g
: x86アーキテクチャ向けのコード生成バックエンド。
これらのバックエンドは、gc
が生成した中間表現を受け取り、それぞれのアーキテクチャに特化した最適化や命令選択を行います。
OEFACE
ノード
Goコンパイラの内部では、抽象構文木(AST)のノードとして様々な操作が表現されます。OEFACE
は、このコミットで新しく導入されたノードタイプで、空のインターフェース値の「itable(型情報)とデータワード」を表現します。これは、コンパイラがインターフェース値を直接構築するための新しい中間表現です。
技術的詳細
このコミットの主要な技術的変更点は、convT2E
変換の最適化、特にuintptr-shaped
な型に対するインライン化です。
従来のconvT2E
変換は、runtime
パッケージ内のヘルパー関数(例: runtime.convT2E
)を呼び出すことで行われていました。これは汎用的な処理を提供しますが、関数呼び出しのオーバーヘッドや、型情報と値のコピー処理が常に発生するという非効率性がありました。
このコミットでは、以下の条件が満たされる場合に、convT2E
変換をコンパイラが直接コード生成するように変更します。
- 変換元がインターフェース型ではないこと (
!isinter(n->left->type)
): 既にインターフェースであるものを空インターフェースに変換するケースは対象外。 - 変換先が空インターフェース型であること (
isnilinter(n->type)
):interface{}
への変換であること。 - 変換元の型がポインタサイズと同じ幅であること (
n->left->type->width == widthptr
):uintptr-shaped
であることの条件。 - 変換元の型が整数型に似ていること (
isint[simsimtype(n->left->type)]
): これは、値が直接コピー可能であることを示唆します。
これらの条件が満たされる場合、コンパイラはOCONVIFACE
ノードを新しいOEFACE
ノードに変換します。OEFACE
ノードは、変換元の型情報と値を直接インターフェース値の2つのワードにマッピングするようコンパイラに指示します。これにより、runtime
関数呼び出しのオーバーヘッドが削減され、より効率的なコードが生成されます。
具体的には、src/cmd/gc/walk.c
のwalkexpr
関数内で、OCONVIFACE
ノードの処理に上記の最適化ロジックが追加されています。最適化が適用される場合、nod(OEFACE, typename(n->left->type), n->left)
という形で新しいOEFACE
ノードが構築され、元のOCONVIFACE
ノードを置き換えます。
また、src/cmd/gc/gen.c
には、cgen_eface
という新しい関数が追加されています。この関数は、OEFACE
ノードを受け取り、その型情報と値を、結果のインターフェース値の2つのワードに直接コピーするコードを生成します。これにより、uintptr-shaped
な値が空インターフェースに変換される際に、効率的な2ワードコピーが実現されます。
各アーキテクチャ固有のコードジェネレータ(src/cmd/5g/cgen.c
, src/cmd/6g/cgen.c
, src/cmd/8g/cgen.c
)も、新しく導入されたOEFACE
ノードを適切に処理するように更新されています。これにより、OEFACE
ノードが最終的な機械語に正しく変換されることが保証されます。
ベンチマーク結果を見ると、BenchmarkConvT2EUintptr
が9ns/opから0ns/opへと劇的に改善しており、この最適化が非常に効果的であったことがわかります。これは、uintptr
のような単純な値のインターフェース変換が、ほぼゼロコストで実行できるようになったことを示唆しています。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルと、その役割は以下の通りです。
src/cmd/5g/cgen.c
,src/cmd/6g/cgen.c
,src/cmd/8g/cgen.c
:- 各アーキテクチャ(ARM, AMD64, x86)のコードジェネレータ。
- 新しく導入された
OEFACE
ノードを処理するためのcgen
およびagen
関数にケースが追加されています。これにより、OEFACE
ノードがコンパイル時に適切に扱われるようになります。
src/cmd/gc/gen.c
:- Goコンパイラの汎用コード生成部分。
cgen_eface
という新しい関数が追加されています。この関数は、OEFACE
ノードを受け取り、空インターフェースの型情報とデータワードを生成するコードを直接生成します。
src/cmd/gc/go.h
:- Goコンパイラのヘッダーファイル。
OEFACE
という新しいASTノードタイプがenum
に追加され、cgen_eface
関数のプロトタイプが宣言されています。
src/cmd/gc/walk.c
:- GoコンパイラのASTウォーク(走査)と最適化を行う部分。
walkexpr
関数内のOCONVIFACE
ケースに、uintptr-shaped
な型から空インターフェースへの変換を最適化するロジックが追加されています。条件が満たされる場合、OCONVIFACE
ノードがOEFACE
ノードに変換されます。
src/pkg/runtime/iface_test.go
:- Goランタイムのインターフェース関連のベンチマークテスト。
BenchmarkConvT2EUintptr
という新しいベンチマークが追加され、uintptr
型から空インターフェースへの変換性能を測定します。既存のベンチマークも調整されています。
test/convT2E.go
:- 新しいテストファイル。
- 様々な型から空インターフェースへの変換が正しく行われることを検証するテストケースが含まれています。特に、
uintptr-shaped
な型を含む多くの型がテストされています。
コアとなるコードの解説
src/cmd/gc/walk.c
の変更点
case OCONVIFACE:
walkexpr(&n->left, init);
// Optimize convT2E as a two-word copy when T is uintptr-shaped.
if(!isinter(n->left->type) && isnilinter(n->type) &&
(n->left->type->width == widthptr) &&
isint[simsimtype(n->left->type)]) {
l = nod(OEFACE, typename(n->left->type), n->left);
l->type = n->type;
l->typecheck = n->typecheck;
n = l;
goto ret;
}
// Build name of function: convI2E etc.
// Not all names are possible
// (e.g., we'll never generate convE2E or convE2I).
// walkexpr(&n->left, init); // Moved above
strcpy(buf, "conv");
// ... (rest of the original code)
この部分が最適化の核心です。OCONVIFACE
(型変換)ノードを処理する際に、以下の条件をチェックします。
!isinter(n->left->type)
: 変換元がインターフェース型ではないこと。isnilinter(n->type)
: 変換先が空インターフェース(interface{}
)であること。(n->left->type->width == widthptr)
: 変換元の型がポインタサイズと同じ幅であること(uintptr-shaped
)。isint[simsimtype(n->left->type)]
: 変換元の型が整数型に似ていること。
これらの条件が全て真の場合、コンパイラはnod(OEFACE, typename(n->left->type), n->left)
を使って新しいOEFACE
ノードを生成します。typename(n->left->type)
は変換元の型の型情報を表し、n->left
は変換元の値を表します。この新しいOEFACE
ノードが元のOCONVIFACE
ノードを置き換え、後続のコード生成フェーズで最適化された処理が行われるようにします。
src/cmd/gc/gen.c
の cgen_eface
関数
/*
* generate:
* res = iface{typ, data}
* n->left is typ
* n->right is data
*/
void
cgen_eface(Node *n, Node *res)
{
Node dst;
dst = *res;
dst.type = types[tptr];
cgen(n->left, &dst);
dst.xoffset += widthptr;
cgen(n->right, &dst);
}
この新しい関数は、OEFACE
ノードのコード生成を担当します。OEFACE
ノードは、n->left
に型情報、n->right
に値を持っています。
dst = *res;
: 結果を格納するノードres
をコピーしてdst
を作成します。dst.type = types[tptr];
:dst
の型をポインタ型に設定します。これは、インターフェースの最初のワード(型情報)がポインタであることを示します。cgen(n->left, &dst);
:n->left
(型情報)をdst
にコピーするコードを生成します。これにより、インターフェース値の最初のワードに型情報が格納されます。dst.xoffset += widthptr;
:dst
のオフセットをポインタの幅(widthptr
)だけ進めます。これにより、次の書き込み位置がインターフェース値の2番目のワード(値)になります。cgen(n->right, &dst);
:n->right
(値)をdst
にコピーするコードを生成します。これにより、インターフェース値の2番目のワードに値が格納されます。
このcgen_eface
関数により、uintptr-shaped
な値から空インターフェースへの変換が、型情報と値の2つのワードを直接コピーする効率的な操作としてコンパイルされます。
関連リンク
- Go言語のインターフェースに関する公式ドキュメントやブログ記事
- Goコンパイラの内部構造に関する資料(Goのソースコードリポジトリ内の
src/cmd/gc/
ディレクトリのドキュメントなど)
参考にした情報源リンク
- Go言語のソースコード (特に
src/cmd/gc/
,src/pkg/runtime/
) - Go言語のベンチマーク結果 (コミットメッセージに記載されているもの)
- Go言語のインターフェース実装に関する一般的な知識
- Goの型システムに関するドキュメント