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

[インデックス 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構造体)に基づいて行われるため、もし異なるインターフェース型I1I2が同じ具象型Xを保持しており、かつI1I2がメソッドセットが同一であるためにeqtype(型が等しいかどうかのチェック)でtrueを返してしまうと、I1itabI2の値にも誤って適用されてしまう可能性がありました。これは、インターフェースの比較(==演算子)が、内部の具象値とitabの両方を比較することで行われるため、予期せぬ結果を招く可能性がありました。

Issue #7207は、まさにこの問題、すなわち異なるインターフェース型間でitabが誤って共有され、インターフェースの比較が期待通りに動作しないケースを報告しています。このコミットは、この根本原因に対処し、インターフェース比較のセマンティクスが正しく維持されるようにします。

前提知識の解説

Goのインターフェース

Goのインターフェースは、メソッドのセットを定義する型です。インターフェース型は、そのメソッドセットを実装する任意の具象型の値を保持できます。インターフェース値は、内部的に以下の2つの要素で構成されます。

  1. 型情報 (type descriptor): インターフェース値が現在保持している具象型の情報(_type構造体へのポインタ)。
  2. データ (data pointer): インターフェース値が現在保持している具象値へのポインタ。

空インターフェース(interface{})の場合、型情報とデータポインタのみを持ちます。一方、非空インターフェース(メソッドを持つインターフェース)の場合、型情報ポインタは実際には「インターフェーステーブル」(itab)へのポインタとなります。

インターフェーステーブル (itab)

itabは、Goランタイムがインターフェースのメソッド呼び出しを効率的にディスパッチするために使用する内部データ構造です。itabは、特定の具象型が特定のインターフェース型をどのように実装しているかに関する情報を含んでいます。具体的には、以下の情報が含まれます。

  • インターフェース型へのポインタ
  • 具象型へのポインタ
  • ハッシュ値
  • メソッドのオフセットと関数ポインタの配列

Goランタイムは、インターフェース値が作成される際や、型アサーションが行われる際に、対応するitabを検索または生成し、キャッシュします。これにより、同じインターフェース型と具象型の組み合わせに対して何度もitabを計算するオーバーヘッドを避けることができます。

eqtype関数

eqtypeはGoコンパイラの内部関数で、2つの型が等しいかどうかを判断します。Goの型システムでは、型名が異なっていても、その構造やメソッドセットが同一であれば型が等しいとみなされる場合があります。例えば、type MyInt inttype 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を適用していました。

  1. srcdstの基底型(orig)が等しい場合。
  2. srcまたはdstが名前付き型ではない場合(sym == Sはシンボルがないことを意味し、匿名型を示唆)。
  3. srcがインターフェース型である場合(src->etype == TINTER)。

問題は3番目の条件、src->etype == TINTERにありました。これにより、srcがインターフェース型であれば、dstが別のインターフェース型であっても、eqtype(src->orig, dst->orig)trueを返す限り、nop-convertが適用されていました。

しかし、Goのitabキャッシュはインターフェース型そのもの(Type構造体)をキーとしています。I1I2という2つの異なるインターフェース型が、たまたま同じメソッドセットを持ち、eqtype(I1, I2)trueを返したとしても、これらはGoの型システム上は異なる型です。I1のために生成されたitabI2の値に適用すると、インターフェースの比較(==)が期待通りに動作しなくなる可能性があります。インターフェースの比較は、内部の具象値とitabの両方を比較するため、itabが異なる型間で共有されると、論理的に異なるインターフェース値が等しいと判断されたり、その逆が発生したりする可能性があります。

このコミットでは、assignop関数の条件を以下のように変更しました。

if(eqtype(src->orig, dst->orig) && (src->sym == S || dst->sym == S || isnilinter(src)))

変更点は、src->etype == TINTERisnilinter(src)に置き換えられたことです。

  • isnilinter(src): これは、srcが空インターフェース型(interface{})であるかどうかをチェックする関数です。

この変更により、nop-convertが適用される条件がより厳しくなりました。具体的には、srcが非空インターフェース型である場合、たとえsrcdstの基底型が等しくても、nop-convertは適用されなくなります。これにより、異なる非空インターフェース型間での変換時には、常に新しいitabが計算されるか、既存の適切なitabが検索されるようになります。

この修正は、インターフェースの比較がitabの同一性に依存しているというGoランタイムの内部的な不変条件を維持するために不可欠です。異なるインターフェース型は、たとえメソッドセットが同一であっても、異なるitabを持つべきであり、それによってインターフェースの比較が型セマンティクスに沿って正しく行われるようになります。

test/cmp.goに追加されたテストケースは、この問題が修正されたことを検証します。I1I2という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)

このテストコードは、I1I2という2つの異なる名前のインターフェース型を定義していますが、これらは同じメソッドx()を持つため、メソッドセットは等価です。Xという具象型がこのx()メソッドを実装しています。

テストでは、これらのインターフェース型と具象型を使って様々なインターフェース値を生成し、それらの比較結果を検証しています。特に注目すべきは、a1 == a2のような、異なるインターフェース型を持つが内部の具象値が同じであるインターフェース値の比較がtrueになることを期待している点です。これは、itabが正しく管理され、インターフェースの比較が期待通りに機能していることを示します。

コアとなるコードの解説

src/cmd/gc/subr.cassignop関数は、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の新しいテストケースは、この修正の有効性を具体的に示しています。I1I2という異なるインターフェース型が同じ具象型Xをラップしている場合でも、それらの比較が期待通りに動作することを確認しています。これは、itabが正しく区別され、インターフェースの同一性チェックが厳密に行われていることを意味します。

関連リンク

参考にした情報源リンク

  • Go言語のインターフェースの内部構造に関する一般的な情報源(例: Goの公式ドキュメント、Goのソースコード解説ブログなど)
  • Goコンパイラ(cmd/gc)のassignop関数に関する情報源
  • Goのitabに関する詳細な解説記事
  • Goの型システムとeqtypeの動作に関する情報源