[インデックス 18671] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)におけるインターフェース型の扱い、特にインターフェースの比較と内部表現(itab)に関するバグ修正と改善を目的としています。異なるが等価なインターフェース型間でnop-convert
(no-operation conversion、何もしない変換)が行われないようにすることで、インターフェース比較の不変性を保ち、itab
のキャッシュが正しく機能するように修正されています。
コミット
commit e5f01aee04dc6313c85dab78305adf499e1f7bfa
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Thu Feb 27 08:07:50 2014 +0100
cmd/gc: do not nop-convert equivalent but different interface types.
The cached computed interface tables are indexed by the interface
types, not by the unnamed underlying interfaces
To preserve the invariants expected by interface comparison, an
itab generated for an interface type must not be used for a value
of a different interface type even if the representation is identical.
Fixes #7207.
LGTM=rsc
R=rsc, iant, khr
CC=golang-codereviews
https://golang.org/cl/69210044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e5f01aee04dc6313c85dab78305adf499e1f7bfa
元コミット内容
cmd/gc: do not nop-convert equivalent but different interface types.
このコミットは、Goコンパイラ(cmd/gc
)において、等価ではあるが異なるインターフェース型間の変換時に、nop-convert
(何もしない変換)を行わないように変更します。これは、キャッシュされたインターフェーステーブル(itab
)がインターフェース型によってインデックス付けされており、その内部表現が同一であっても、異なるインターフェース型間でitab
を再利用すべきではないという原則に基づいています。インターフェース比較の不変性を維持するために、あるインターフェース型のために生成されたitab
は、表現が同一であっても別のインターフェース型の値には使用されないようにします。
この変更は、Go issue #7207 を修正します。
変更の背景
Go言語において、インターフェースは動的な型情報とメソッドセットを抽象化するための強力な機能です。Goのインターフェース値は、内部的に2つのポインタで構成されます。1つは基底となる具象型の値へのポインタ、もう1つは「インターフェーステーブル」(itab
)へのポインタです。itab
は、そのインターフェース型が具象型によってどのように実装されているか(どのメソッドがどの関数に対応するかなど)に関する情報を含んでいます。
Goランタイムは、インターフェースの型アサーションや型変換のパフォーマンスを最適化するために、itab
をキャッシュします。このキャッシュは、インターフェース型をキーとしてitab
を格納します。しかし、Goの型システムでは、異なる名前を持つインターフェース型であっても、メソッドセットが完全に同一であれば「等価」とみなされる場合があります。
このコミット以前は、コンパイラがこのような「等価だが異なる」インターフェース型間の変換をnop-convert
として扱ってしまうことがありました。nop-convert
とは、コンパイラが変換が不要であると判断し、実際には何もコードを生成しない最適化です。これは通常、パフォーマンス向上に寄与しますが、インターフェースの比較においては問題を引き起こす可能性がありました。
具体的には、itab
のキャッシュがインターフェース型(Type
構造体)に基づいて行われるため、もし異なるインターフェース型I1
とI2
が同じ具象型X
を保持しており、かつI1
とI2
がメソッドセットが同一であるためにeqtype
(型が等しいかどうかのチェック)でtrue
を返してしまうと、I1
のitab
がI2
の値にも誤って適用されてしまう可能性がありました。これは、インターフェースの比較(==
演算子)が、内部の具象値とitab
の両方を比較することで行われるため、予期せぬ結果を招く可能性がありました。
Issue #7207は、まさにこの問題、すなわち異なるインターフェース型間でitab
が誤って共有され、インターフェースの比較が期待通りに動作しないケースを報告しています。このコミットは、この根本原因に対処し、インターフェース比較のセマンティクスが正しく維持されるようにします。
前提知識の解説
Goのインターフェース
Goのインターフェースは、メソッドのセットを定義する型です。インターフェース型は、そのメソッドセットを実装する任意の具象型の値を保持できます。インターフェース値は、内部的に以下の2つの要素で構成されます。
- 型情報 (type descriptor): インターフェース値が現在保持している具象型の情報(
_type
構造体へのポインタ)。 - データ (data pointer): インターフェース値が現在保持している具象値へのポインタ。
空インターフェース(interface{}
)の場合、型情報とデータポインタのみを持ちます。一方、非空インターフェース(メソッドを持つインターフェース)の場合、型情報ポインタは実際には「インターフェーステーブル」(itab
)へのポインタとなります。
インターフェーステーブル (itab)
itab
は、Goランタイムがインターフェースのメソッド呼び出しを効率的にディスパッチするために使用する内部データ構造です。itab
は、特定の具象型が特定のインターフェース型をどのように実装しているかに関する情報を含んでいます。具体的には、以下の情報が含まれます。
- インターフェース型へのポインタ
- 具象型へのポインタ
- ハッシュ値
- メソッドのオフセットと関数ポインタの配列
Goランタイムは、インターフェース値が作成される際や、型アサーションが行われる際に、対応するitab
を検索または生成し、キャッシュします。これにより、同じインターフェース型と具象型の組み合わせに対して何度もitab
を計算するオーバーヘッドを避けることができます。
eqtype
関数
eqtype
はGoコンパイラの内部関数で、2つの型が等しいかどうかを判断します。Goの型システムでは、型名が異なっていても、その構造やメソッドセットが同一であれば型が等しいとみなされる場合があります。例えば、type MyInt int
とtype YourInt int
は異なる名前の型ですが、eqtype
はこれらを等価と判断する可能性があります(ただし、このコミットの文脈ではインターフェース型に焦点が当てられています)。
OCONVNOP
(No-Operation Conversion)
OCONVNOP
は、Goコンパイラが型変換を行う際に使用する内部的な操作コードの一つです。これは、ソース型とターゲット型が実質的に同じ表現を持つ場合、または変換が不要であるとコンパイラが判断した場合に適用されます。OCONVNOP
が適用されると、コンパイル時に実際の変換コードは生成されず、実行時のオーバーヘッドが削減されます。これは最適化の一種ですが、インターフェースの比較のようなセマンティクスが重要な場面では、意図しない動作を引き起こす可能性があります。
Issue 7207
Go issue #7207は、「interface comparisons (issue 7207)」というコメントがテストコードに追加されていることから、このコミットが修正しようとしている具体的なバグです。この問題は、異なるインターフェース型が同じメソッドセットを持つ場合に、インターフェースの比較が期待通りに動作しないというものでした。これは、コンパイラがこれらの型間の変換をnop-convert
として扱い、結果としてitab
が誤って共有されてしまうことに起因していました。
技術的詳細
このコミットの核心は、src/cmd/gc/subr.c
内のassignop
関数における型変換のロジック変更です。assignop
関数は、Goコンパイラが代入操作や型変換の際に、ソース型(src
)からデスティネーション型(dst
)への変換方法を決定する役割を担っています。
変更前のコードでは、以下の条件が満たされた場合にOCONVNOP
(何もしない変換)を返していました。
if(eqtype(src->orig, dst->orig) && (src->sym == S || dst->sym == S || src->etype == TINTER))
この条件は、以下のいずれかのケースでOCONVNOP
を適用していました。
src
とdst
の基底型(orig
)が等しい場合。src
またはdst
が名前付き型ではない場合(sym == S
はシンボルがないことを意味し、匿名型を示唆)。src
がインターフェース型である場合(src->etype == TINTER
)。
問題は3番目の条件、src->etype == TINTER
にありました。これにより、src
がインターフェース型であれば、dst
が別のインターフェース型であっても、eqtype(src->orig, dst->orig)
がtrue
を返す限り、nop-convert
が適用されていました。
しかし、Goのitab
キャッシュはインターフェース型そのもの(Type
構造体)をキーとしています。I1
とI2
という2つの異なるインターフェース型が、たまたま同じメソッドセットを持ち、eqtype(I1, I2)
がtrue
を返したとしても、これらはGoの型システム上は異なる型です。I1
のために生成されたitab
をI2
の値に適用すると、インターフェースの比較(==
)が期待通りに動作しなくなる可能性があります。インターフェースの比較は、内部の具象値とitab
の両方を比較するため、itab
が異なる型間で共有されると、論理的に異なるインターフェース値が等しいと判断されたり、その逆が発生したりする可能性があります。
このコミットでは、assignop
関数の条件を以下のように変更しました。
if(eqtype(src->orig, dst->orig) && (src->sym == S || dst->sym == S || isnilinter(src)))
変更点は、src->etype == TINTER
がisnilinter(src)
に置き換えられたことです。
isnilinter(src)
: これは、src
が空インターフェース型(interface{}
)であるかどうかをチェックする関数です。
この変更により、nop-convert
が適用される条件がより厳しくなりました。具体的には、src
が非空インターフェース型である場合、たとえsrc
とdst
の基底型が等しくても、nop-convert
は適用されなくなります。これにより、異なる非空インターフェース型間での変換時には、常に新しいitab
が計算されるか、既存の適切なitab
が検索されるようになります。
この修正は、インターフェースの比較がitab
の同一性に依存しているというGoランタイムの内部的な不変条件を維持するために不可欠です。異なるインターフェース型は、たとえメソッドセットが同一であっても、異なるitab
を持つべきであり、それによってインターフェースの比較が型セマンティクスに沿って正しく行われるようになります。
test/cmp.go
に追加されたテストケースは、この問題が修正されたことを検証します。I1
とI2
という2つの異なるインターフェース型が、同じメソッドx()
を持つX
という具象型を実装している状況をシミュレートしています。これらのインターフェース値がどのように比較されるかを詳細にテストし、修正が正しく機能していることを確認しています。特に、a1 == a2
のような比較がtrue
になることを期待しており、これはI1(X(0))
とI2(X(0))
が異なるインターフェース型であるにもかかわらず、内部の具象値とitab
が正しく比較されることを示唆しています。
コアとなるコードの変更箇所
src/cmd/gc/subr.c
--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -1223,8 +1223,10 @@ assignop(Type *src, Type *dst, char **why)
// 2. src and dst have identical underlying types
// and either src or dst is not a named type or
- // both are interface types.
- if(eqtype(src->orig, dst->orig) && (src->sym == S || dst->sym == S || src->etype == TINTER))
+ // both are empty interface types.
+ // For assignable but different non-empty interface types,
+ // we want to recompute the itab.
+ if(eqtype(src->orig, dst->orig) && (src->sym == S || dst->sym == S || isnilinter(src)))
return OCONVNOP;
// 3. dst is an interface type and src implements dst.
この変更は、assignop
関数内のOCONVNOP
を返す条件を修正しています。
変更前: src->etype == TINTER
変更後: isnilinter(src)
これにより、「src
がインターフェース型である」という広範な条件から、「src
が空インターフェース型である」というより具体的な条件に絞り込まれました。コメントも追加され、非空インターフェース型間ではitab
を再計算する必要があることが明記されています。
test/cmp.go
--- a/test/cmp.go
+++ b/test/cmp.go
@@ -35,6 +35,10 @@ func istrue(b bool) {
type T *int
+type X int
+
+func (X) x() {}
+
func main() {
var a []int
var b map[string]int
@@ -129,6 +133,44 @@ func main() {
panic("bad m[c]")
}
+ // interface comparisons (issue 7207)
+ {
+ type I1 interface {
+ x()
+ }
+ type I2 interface {
+ x()
+ }
+ a1 := I1(X(0))
+ b1 := I1(X(1))
+ a2 := I2(X(0))
+ b2 := I2(X(1))
+ a3 := I1(a2)
+ a4 := I2(a1)
+ var e interface{} = X(0)
+ a5 := e.(I1)
+ a6 := e.(I2)
+ isfalse(a1 == b1)
+ isfalse(a1 == b2)
+ isfalse(a2 == b1)
+ isfalse(a2 == b2)
+ istrue(a1 == a2)
+ istrue(a1 == a3)
+ istrue(a1 == a4)
+ istrue(a1 == a5)
+ istrue(a1 == a6)
+ istrue(a2 == a3)
+ istrue(a2 == a4)
+ istrue(a2 == a5)
+ istrue(a2 == a6)
+ istrue(a3 == a4)
+ istrue(a3 == a5)
+ istrue(a3 == a6)
+ istrue(a4 == a5)
+ istrue(a4 == a6)
+ istrue(a5 == a6)
+ }
+
// non-interface comparisons
{
c := make(chan int)
このテストコードは、I1
とI2
という2つの異なる名前のインターフェース型を定義していますが、これらは同じメソッドx()
を持つため、メソッドセットは等価です。X
という具象型がこのx()
メソッドを実装しています。
テストでは、これらのインターフェース型と具象型を使って様々なインターフェース値を生成し、それらの比較結果を検証しています。特に注目すべきは、a1 == a2
のような、異なるインターフェース型を持つが内部の具象値が同じであるインターフェース値の比較がtrue
になることを期待している点です。これは、itab
が正しく管理され、インターフェースの比較が期待通りに機能していることを示します。
コアとなるコードの解説
src/cmd/gc/subr.c
のassignop
関数は、Goコンパイラの型システムの中核をなす部分の一つです。この関数は、ある型から別の型への代入や変換がどのように行われるべきかを決定します。
変更前のコードでは、src->etype == TINTER
という条件が問題でした。これは、ソース型がインターフェース型であれば、たとえそれが空インターフェース(interface{}
)でなくても、OCONVNOP
が適用される可能性があったことを意味します。
Goのインターフェースの内部表現において、空インターフェースは具象型と値のポインタを直接持ちます。一方、非空インターフェースは具象型と値のポインタに加えて、itab
へのポインタを持ちます。itab
はインターフェース型と具象型の組み合わせごとに生成され、キャッシュされます。
このコミットの修正は、itab
のキャッシュがインターフェース型そのもの(Type
構造体)をキーとしており、異なるインターフェース型は異なるitab
を持つべきであるという原則を厳密に適用します。
isnilinter(src)
への変更は、OCONVNOP
を適用する条件を「ソース型が空インターフェースである場合」に限定します。空インターフェースはメソッドを持たないため、itab
は不要であり、その変換は常にnop-convert
で問題ありません。しかし、非空インターフェース型の場合、たとえメソッドセットが同一であっても、異なるインターフェース型は異なるitab
を持つべきです。これにより、インターフェースの比較が、内部の具象値だけでなく、インターフェース型と具象型の組み合わせを正確に反映したitab
に基づいて行われるようになります。
この修正により、コンパイラは異なる非空インターフェース型間の変換時に、itab
の再計算または適切なitab
の検索を強制するようになります。これにより、Goのインターフェース比較のセマンティクスが正しく維持され、Issue #7207で報告されたようなバグが解消されます。
test/cmp.go
の新しいテストケースは、この修正の有効性を具体的に示しています。I1
とI2
という異なるインターフェース型が同じ具象型X
をラップしている場合でも、それらの比較が期待通りに動作することを確認しています。これは、itab
が正しく区別され、インターフェースの同一性チェックが厳密に行われていることを意味します。
関連リンク
- Go issue #7207: https://github.com/golang/go/issues/7207
- Go CL 69210044: https://golang.org/cl/69210044
参考にした情報源リンク
- Go言語のインターフェースの内部構造に関する一般的な情報源(例: Goの公式ドキュメント、Goのソースコード解説ブログなど)
- Goコンパイラ(
cmd/gc
)のassignop
関数に関する情報源 - Goの
itab
に関する詳細な解説記事 - Goの型システムと
eqtype
の動作に関する情報源