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

[インデックス 14323] ファイルの概要

このコミットは、Go言語のコンパイラ (cmd/gc) とランタイム (src/pkg/runtime) における型アサーションの最適化に関するものです。具体的には、型アサーションの結果として得られる値がブランク識別子 (_) によって破棄される場合に、不要なデータコピーが発生する問題を解決します。これにより、特に大きな非インターフェース型を扱う際のパフォーマンスが向上します。

コミット

commit d098bffd8488df939221bc487cf6f2f124b66e1e
Author: Daniel Morsing <daniel.morsing@gmail.com>
Date:   Tue Nov 6 20:40:40 2012 +0100

    cmd/gc, runtime: avoid unnecessary copy on type assertion.
    
    When the first result of a type assertion is blank, the compiler would still copy out a potentially large non-interface type.
    
    Fixes #1021.
    
    R=golang-dev, bradfitz, rsc
    CC=golang-dev
    https://golang.org/cl/6812079

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/d098bffd8488df939221bc487cf6f2f124b66e1e

元コミット内容

cmd/gc, runtime: avoid unnecessary copy on type assertion.

When the first result of a type assertion is blank, the compiler would still copy out a potentially large non-interface type.

Fixes #1021.

R=golang-dev, bradfitz, rsc
CC=golang-dev
https://golang.org/cl/6812079

変更の背景

Go言語の型アサーションは、インターフェース型の変数が保持する基底の具象型をチェックし、その値を取り出すための強力な機能です。通常、value, ok := interfaceValue.(Type) の形式で使われ、value にはアサートされた値が、ok にはアサーションの成否がブール値で返されます。

このコミットが修正する問題は、value の部分がブランク識別子 (_) で破棄される場合、つまり _, ok := interfaceValue.(Type) のように、アサートされた値自体は不要で、アサーションの成否 (ok) のみが必要なケースで発生していました。

従来のコンパイラの挙動では、たとえアサートされた値がブランク識別子で破棄されるとしても、コンパイラは依然としてその値をメモリ上にコピーしようとしていました。特に、アサート対象の型が大きな構造体や配列などの非インターフェース型である場合、この不要なコピーは無視できないパフォーマンスオーバーヘッドを引き起こしていました。

この問題はGoのIssue #1021として報告されており、このコミットはその解決を目的としています。不要なコピーを排除することで、特にパフォーマンスが重視される場面での型アサーションの効率を向上させることが期待されます。

前提知識の解説

Goの型アサーション (Type Assertion)

Goにおける型アサーションは、インターフェース型の変数が実際にどのような具象型の値を保持しているかを確認し、その具象型の値を取り出すための構文です。

基本的な形式は以下の通りです。

  1. 単一の結果: t := i.(T)

    • i はインターフェース型の変数。
    • T は具象型(例: string, int, MyStruct)または別のインターフェース型。
    • iT 型の値を保持していない場合、ランタイムパニックが発生します。
  2. 「カンマOK」イディオム: t, ok := i.(T)

    • この形式は、アサーションの成否を安全に確認するために使用されます。
    • okbool 型で、アサーションが成功した場合は true、失敗した場合は false になります。
    • アサーションが失敗してもパニックは発生せず、t には T 型のゼロ値が代入されます。
    • この形式は、通常 if 文と組み合わせて使用されます。
      var i interface{} = "hello"
      if s, ok := i.(string); ok {
          fmt.Println("iはstring型です:", s)
      } else {
          fmt.Println("iはstring型ではありません")
      }
      

Goのブランク識別子 (_)

ブランク識別子 (_) は、Goにおいて値が必要とされるが、その値自体は使用されないことを明示するために使われる特別な識別子です。

主な用途は以下の通りです。

  • 戻り値の破棄: 関数が複数の値を返す場合、不要な戻り値を破棄するために使用します。
    _, err := someFunction() // エラーのみに関心がある場合
    
  • インポートの副作用: パッケージの init 関数など、パッケージのインポート自体に副作用がある場合に、そのパッケージの識別子を直接使用しないことを示すために使用します。
    import _ "net/http/pprof"
    
  • ループ変数の破棄: for...range ループで、インデックスまたは値のいずれかのみが必要な場合に使用します。
    for _, value := range slice { // インデックスが不要な場合
        fmt.Println(value)
    }
    
  • 未使用変数の回避: 宣言された変数が使用されていない場合に発生するコンパイルエラーを回避するために使用します。

インターフェース型から非インターフェース型への変換とコピー

Goのインターフェースは、内部的に「型情報」と「値」のペアとして表現されます。インターフェース型から具象型への型アサーションを行う際、特にインターフェースが保持する値がポインタではなく直接値である場合、その値が新しい具象型の変数にコピーされることがあります。

このコピーは、値が小さい場合は問題になりませんが、大きな構造体や配列の場合には、メモリ割り当てとコピーのオーバーヘッドがパフォーマンスに影響を与える可能性があります。本コミットは、この「不要なコピー」を特定し、回避することを目的としています。

技術的詳細

このコミットの核心は、コンパイラが型アサーションを処理する方法を変更し、特にブランク識別子が使用されている場合に、不要なデータコピーを避けるための新しいランタイム関数を導入した点にあります。

1. 新しいランタイム関数の導入

型アサーションの成否のみが必要なケース (_, ok := i.(T)) に特化した、新しい内部ランタイム関数が導入されました。

  • runtime·assertI2TOK(typ *byte, iface any) (ok bool): インターフェース型 (Iface) から具象型へのアサーションで、成否 (ok) のみが必要な場合に使用されます。
  • runtime·assertE2TOK(typ *byte, iface any) (ok bool): 空インターフェース型 (Eface) から具象型へのアサーションで、成否 (ok) のみが必要な場合に使用されます。

これらの関数は、従来の assertI2T2assertE2T2 と異なり、アサートされた値をコピーする copyout 処理を含みません。これにより、不要なメモリ割り当てとデータコピーが回避されます。

2. コンパイラ (cmd/gc) の変更

src/cmd/gc/walk.c は、Goのコンパイラのバックエンドの一部であり、抽象構文木 (AST) を走査し、コード生成のための準備を行う役割を担っています。このファイルが変更され、型アサーションの処理ロジックが更新されました。

変更点:

  • 型アサーション (ODOTTYPE2 ノード) を処理する walkexpr 関数内で、アサーションの最初の結果がブランク識別子 (isblank(n->list->n)) であり、かつアサート対象の型がインターフェース型ではない (!isinter(r->type)) 場合を検出するロジックが追加されました。
  • この条件に合致する場合、コンパイラは従来の assertI2T2assertE2T2 ではなく、新しく導入された assertI2TOK または assertE2TOK ランタイム関数を呼び出すようにコードを生成します。
  • 具体的には、syslook を使って適切なランタイム関数名(例: "assertI2TOK")を検索し、その関数を呼び出す OCALL ノードを構築します。この呼び出しには、アサート対象の型情報とインターフェース値が引数として渡されます。
  • これにより、アサートされた値が不要な場合に、その値のコピー処理が完全にスキップされるようになります。

3. ランタイム (src/pkg/runtime) の変更

src/pkg/runtime/iface.c は、Goのランタイムにおけるインターフェース関連の処理を実装しています。このファイルに、runtime·assertI2TOKruntime·assertE2TOK の具体的な実装が追加されました。

実装詳細:

  • runtime·assertI2TOK:
    • ok = i.tab!=nil && i.tab->type==t; というロジックで、インターフェース i の型情報 (i.tab->type) が期待される型 t と一致するかどうかをチェックします。
    • FLUSH(&ok); は、ok 変数の値が確実にメモリに書き込まれるようにするためのランタイム内部の操作です。
    • この関数は、copyout を明示的に呼び出していません。
  • runtime·assertE2TOK:
    • ok = t==e.type; というロジックで、空インターフェース e の型情報 (e.type) が期待される型 t と一致するかどうかをチェックします。
    • 同様に FLUSH(&ok); を含みます。
    • この関数も copyout を呼び出していません。

4. 組み込み関数の宣言 (src/cmd/gc/builtin.c, src/cmd/gc/runtime.go)

新しいランタイム関数がコンパイラから呼び出せるように、src/cmd/gc/builtin.csrc/cmd/gc/runtime.go にこれらの関数の宣言が追加されました。これにより、コンパイラはこれらの関数が存在し、適切なシグネチャを持つことを認識できます。

これらの変更により、Goコンパイラは、型アサーションのセマンティクスを維持しつつ、不要なデータコピーを賢く回避できるようになり、結果としてアプリケーションのパフォーマンスが向上します。

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

このコミットでは、以下の4つのファイルが変更されています。

  • src/cmd/gc/builtin.c: 2行追加
    • 新しいランタイム関数 assertI2TOKassertE2TOK の宣言が追加されました。
  • src/cmd/gc/runtime.go: 2行追加
    • builtin.c と同様に、Goのランタイムパッケージ内でこれらの関数のシグネチャが宣言されました。
  • src/cmd/gc/walk.c: 25行追加
    • コンパイラの型アサーション処理ロジックが変更され、ブランク識別子を使用するケースで新しいランタイム関数を呼び出すように修正されました。
  • src/pkg/runtime/iface.c: 14行追加
    • runtime·assertI2TOKruntime·assertE2TOK の具体的な実装が追加されました。これらの関数は、アサーションの成否のみを返し、不要なデータコピーを行いません。

コアとなるコードの解説

src/cmd/gc/walk.c の変更 (コンパイラのロジック)

// ... (既存のコード) ...

	// Type assertion (ODOTTYPE2)
	// n->list: first result (value), second result (ok)
	// n->rlist->n: interface value being asserted
	// r->type: target type for assertion
	if(n->op == ODOTTYPE2) {
		n->ninit = nil;
		r = n->rlist->n;
		walkexprlistsafe(n->list, init); // Walk the result list (value, ok)

		// If the first result (value) is blank and the target type is not an interface
		if(isblank(n->list->n) && !isinter(r->type)) {
			strcpy(buf, "assert");
			p = buf+strlen(buf);
			if(isnilinter(r->left->type)) // Check if it's an empty interface (Eface)
				*p++ = 'E';
			else // Otherwise, it's a regular interface (Iface)
				*p++ = 'I';
			*p++ = '2';
			*p++ = 'T';
			*p++ = 'O';
			*p++ = 'K';
			*p = '\0'; // Null terminate the string (e.g., "assertI2TOK" or "assertE2TOK")
			
			fn = syslook(buf, 1); // Look up the runtime function by name
			ll = list1(typename(r->type)); // First argument: type descriptor
			ll = list(ll, r->left); // Second argument: interface value
			argtype(fn, r->left->type); // Set argument types for the function call
			n1 = nod(OCALL, fn, N); // Create a call node
			n1->list = ll; // Set arguments for the call
			n = nod(OAS, n->list->next->n, n1); // Assign the result (ok) to the second blank identifier
			typecheck(&n, Etop); // Type check the new assignment
			walkexpr(&n, init); // Walk the new expression
			goto ret; // Skip the original ODOTTYPE2 handling
		}

		r->op = ODOTTYPE2;
		walkexpr(&r, init);
		ll = ascompatet(n->op, n->list, &r->type, 0, init);
	}
// ... (既存のコード) ...

このコードブロックは、コンパイラが型アサーション (ODOTTYPE2) を処理する際の新しいロジックを示しています。

  1. isblank(n->list->n): アサーションの最初の結果(つまり、アサートされた値)がブランク識別子 (_) であるかをチェックします。
  2. !isinter(r->type): アサート対象の型がインターフェース型ではない(つまり、具象型へのアサーションである)かをチェックします。
  3. これらの条件が両方とも真の場合、コンパイラは新しい最適化パスに入ります。
  4. strcpy(buf, "assert"); ... *p++ = 'K'; *p = '\0';: 呼び出すランタイム関数の名前(assertI2TOK または assertE2TOK)を動的に構築します。インターフェースが空インターフェース (Eface) かどうかで 'E' または 'I' を選択します。
  5. fn = syslook(buf, 1);: 構築した名前でランタイム関数を検索します。
  6. ll = list1(typename(r->type)); ll = list(ll, r->left);: ランタイム関数に渡す引数リストを構築します。これには、アサート対象の型情報と元のインターフェース値が含まれます。
  7. n1 = nod(OCALL, fn, N); n1->list = ll;: ランタイム関数呼び出しのASTノードを作成します。
  8. n = nod(OAS, n->list->next->n, n1);: ランタイム関数の戻り値(ok)を、アサーションの2番目の結果(ブランク識別子の次のノード、つまり ok 変数)に代入するASTノードを作成します。
  9. typecheck(&n, Etop); walkexpr(&n, init);: 新しく生成されたASTノードを型チェックし、さらに走査します。
  10. goto ret;: 最適化されたパスが実行された場合、元の型アサーション処理をスキップして関数を終了します。

src/pkg/runtime/iface.c の変更 (ランタイムの実装)

// ... (既存のコード) ...

// func assertI2TOK(typ *byte, iface any) (ok bool)
void
runtime·assertI2TOK(Type *t, Iface i, bool ok)
{
	// Check if the interface's type table is not nil and its type matches the target type
	ok = i.tab!=nil && i.tab->type==t;
	FLUSH(&ok); // Ensure 'ok' is written to memory
}

// ... (既存のコード) ...

// func assertE2TOK(typ *byte, iface any) (ok bool)
void
runtime·assertE2TOK(Type *t, Eface e, bool ok)
{
	// Check if the empty interface's type matches the target type
	ok = t==e.type;
	FLUSH(&ok); // Ensure 'ok' is written to memory
}

// ... (既存のコード) ...

このコードブロックは、新しいランタイム関数 runtime·assertI2TOKruntime·assertE2TOK の実装を示しています。

  • これらの関数は、インターフェースが保持する型情報 (i.tab->type または e.type) が、アサートしようとしているターゲット型 t と一致するかどうかを単純に比較しています。
  • 比較結果は ok 変数に直接代入されます。
  • 重要なのは、これらの関数には、従来の assertI2T2assertE2T2 に存在した copyout 関数呼び出しがないことです。copyout は、インターフェースが保持する値を具象型の変数にコピーする役割を担っていましたが、assertI2TOKassertE2TOK ではアサートされた値自体は不要なため、このコピー処理が完全に省略されています。
  • FLUSH(&ok); は、コンパイラの最適化によって ok 変数への書き込みが遅延されることを防ぎ、確実にメモリに反映させるためのランタイム内部のメカニズムです。

これらの変更により、Goの型アサーションは、開発者がブランク識別子を使用して値の破棄を意図した場合に、ランタイムのオーバーヘッドを最小限に抑えるように最適化されました。

関連リンク

参考にした情報源リンク