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

[インデックス 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種類あります。

  1. 非空インターフェース (Non-empty Interface): 少なくとも1つのメソッドを持つインターフェースです。例えば io.Reader など。
  2. 空インターフェース (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ビット)とアラインメントを持つことを意味します。例えば、intuintunsafe.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変換をコンパイラが直接コード生成するように変更します。

  1. 変換元がインターフェース型ではないこと (!isinter(n->left->type)): 既にインターフェースであるものを空インターフェースに変換するケースは対象外。
  2. 変換先が空インターフェース型であること (isnilinter(n->type)): interface{} への変換であること。
  3. 変換元の型がポインタサイズと同じ幅であること (n->left->type->width == widthptr): uintptr-shapedであることの条件。
  4. 変換元の型が整数型に似ていること (isint[simsimtype(n->left->type)]): これは、値が直接コピー可能であることを示唆します。

これらの条件が満たされる場合、コンパイラはOCONVIFACEノードを新しいOEFACEノードに変換します。OEFACEノードは、変換元の型情報と値を直接インターフェース値の2つのワードにマッピングするようコンパイラに指示します。これにより、runtime関数呼び出しのオーバーヘッドが削減され、より効率的なコードが生成されます。

具体的には、src/cmd/gc/walk.cwalkexpr関数内で、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.ccgen_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に値を持っています。

  1. dst = *res;: 結果を格納するノードresをコピーしてdstを作成します。
  2. dst.type = types[tptr];: dstの型をポインタ型に設定します。これは、インターフェースの最初のワード(型情報)がポインタであることを示します。
  3. cgen(n->left, &dst);: n->left(型情報)をdstにコピーするコードを生成します。これにより、インターフェース値の最初のワードに型情報が格納されます。
  4. dst.xoffset += widthptr;: dstのオフセットをポインタの幅(widthptr)だけ進めます。これにより、次の書き込み位置がインターフェース値の2番目のワード(値)になります。
  5. 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の型システムに関するドキュメント