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

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

コミット

このコミットは、Goコンパイラ(cmd/8g、386アーキテクチャ向け)におけるcap(CHAN)(チャネルの容量を取得する組み込み関数)のコード生成に関する最適化を目的としています。具体的には、cap(CHAN)の呼び出し時に不要なレジスタの早期割り当てを排除し、caplen(チャネルの長さを取得する組み込み関数)で異なるコードが生成される理由がないという認識に基づいています。これにより、コンパイラがより効率的なコードを生成できるようになります。

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

https://github.com/golang/go/commit/1ec56062ef6b256f8269e2ca8c5477e3a917331a

元コミット内容

commit 1ec56062ef6b256f8269e2ca8c5477e3a917331a
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Tue Jul 1 09:20:51 2014 +0200

    cmd/8g: don't allocate a register early for cap(CHAN).
    
    There is no reason to generate different code for cap and len.
    
    Fixes #8025.
    Fixes #8026.
    
    LGTM=rsc
    R=rsc, iant, khr
    CC=golang-codereviews
    https://golang.org/cl/93570044

変更の背景

この変更の背景には、Goコンパイラがcap(CHAN)len(CHAN)という、チャネルの容量と長さを取得する二つの組み込み関数に対して、非効率的かつ不必要な異なるコードを生成していた問題がありました。コミットメッセージには「caplenで異なるコードを生成する理由はない」と明記されており、これはコンパイラのコード生成ロジックにおける冗長性や最適化の余地を示唆しています。

特に、「don't allocate a register early for cap(CHAN)」という記述から、cap(CHAN)の処理において、本来必要とされるよりも早い段階でレジスタが割り当てられていたことが推測されます。これは、レジスタの利用効率を低下させ、生成されるアセンブリコードの品質に悪影響を与える可能性があります。レジスタはCPUが高速にアクセスできる記憶領域であり、その効率的な利用はプログラムのパフォーマンスに直結します。不必要な早期割り当ては、他の重要な値がレジスタに格納される機会を奪ったり、余分なメモリとのデータ転送(スピル/フィル)を発生させたりする原因となります。

コミットメッセージにはFixes #8025Fixes #8026とありますが、これらの具体的なIssueの詳細は現在のところ公開されている情報からは特定できませんでした。しかし、これらのIssueが、cap(CHAN)のコード生成における非効率性や、それによって引き起こされる潜在的なバグやパフォーマンス問題に関連していた可能性が高いと考えられます。このコミットは、これらの問題を解決し、コンパイラがより洗練された、統一されたコードを生成するように改善することを目的としています。

前提知識の解説

Goコンパイラ (cmd/8g)

Go言語のコンパイラは、Goのソースコードを機械語に変換するツールです。Goの初期のコンパイラは、Plan 9 Cコンパイラツールチェーンをベースにしており、各アーキテクチャに対応するコンパイラが特定の命名規則を持っていました。例えば、8gは386(Intel x86 32-bit)アーキテクチャ向けのGoコンパイラを指します。現代のGoでは、クロスコンパイルが容易になり、go buildコマンドが内部で適切なコンパイラを呼び出すため、ユーザーが直接8gのようなコマンドを意識することは少なくなりましたが、コンパイラの内部構造を理解する上では重要な歴史的背景です。

cap()len()

Goには、組み込み関数としてcap()len()があります。

  • len(v): vの長さ(要素数)を返します。vが文字列、スライス、配列、マップ、チャネルの場合に適用されます。
  • cap(v): vの容量を返します。vがスライスまたはチャネルの場合に適用されます。
    • スライスの場合: スライスの基底配列が保持できる最大要素数。
    • チャネルの場合: チャネルがバッファリングできる要素の最大数。バッファなしチャネルの場合、容量は0です。

このコミットでは、特にチャネルに対するcap()の挙動が問題となっていました。チャネルの容量は、チャネルの内部構造体(hchan)の特定のフィールドに格納されています。

レジスタ割り当て (Register Allocation)

レジスタ割り当ては、コンパイラの最適化フェーズの一つで、プログラムの変数をCPUのレジスタに割り当てるプロセスです。レジスタはメモリよりもはるかに高速にアクセスできるため、頻繁に使用される値をレジスタに保持することでプログラムの実行速度を大幅に向上させることができます。

  • regalloc: コンパイラがレジスタを割り当てるための関数や操作を指します。
  • tempname: 一時的な変数を生成し、それに名前(シンボル)を割り当てる操作。これは通常、レジスタに直接割り当てられる前に、中間表現(IR)レベルで一時的な記憶場所を確保するために使用されます。
  • gmove: ある場所から別の場所へデータを移動する操作。レジスタ間、レジスタとメモリ間などで使用されます。
  • N: Goコンパイラの内部表現における「nil」または「null」に相当する概念で、特定のノードやレジスタが割り当てられていない状態を示します。

チャネル (Channels)

Goにおけるチャネルは、ゴルーチン間で値を送受信するためのパイプのようなものです。チャネルは、Goの並行処理モデルの根幹をなす要素であり、安全なデータ共有と同期を可能にします。チャネルにはバッファリングされたものとされていないものがあり、cap()はバッファリングされたチャネルのバッファサイズを返します。

select ステートメント

selectステートメントは、複数のチャネル操作(送受信)を待機し、準備ができた最初の操作を実行するために使用されます。どのチャネル操作も準備ができていない場合、defaultケースがあればそれが実行され、なければいずれかの操作が準備できるまでブロックします。test/torture.goの変更箇所にあるselectブロックは、cap()の呼び出しがselectステートメント内でどのように扱われるかをテストするためのものです。

技術的詳細

このコミットの技術的な核心は、Goコンパイラのバックエンド、特にsrc/cmd/8g/cgen.cファイルにおけるcap(CHAN)のコード生成ロジックの変更にあります。

変更前のコードでは、cap(CHAN)の値を計算する際に、regalloc(&n1, types[tptr], res);という行がありました。これは、n1というノード(チャネルのポインタを表す)に対して、結果を格納するためのレジスタを早期に割り当てようとするものです。しかし、チャネルの容量は、チャネルのポインタが指す構造体(hchan)の特定のオフセットに格納されている単なる整数値です。この値を読み出すために、すぐにレジスタを割り当てる必要はなく、一時的な場所(メモリ上のスタックなど)に置いておき、後で必要になったときにレジスタにロードする方が効率的な場合があります。

変更後のコードでは、regalloc(&n1, types[tptr], res);tempname(&n1, types[tptr]);に置き換えられています。

  • tempname(&n1, types[tptr]);は、n1を一時的な変数として宣言し、その型をポインタ型(types[tptr])に設定します。この時点では、まだ物理的なレジスタは割り当てられません。
  • 次に、cgen(nl, &n1);でチャネルのポインタ自体をn1に生成します。
  • そして、regalloc(&n2, types[tptr], N);n2という新しいノードに対してレジスタを割り当てます。ここでNが指定されているのは、特定のレジスタを要求するのではなく、コンパイラに最適なレジスタを選択させることを意味します。
  • 最後に、gmove(&n1, &n2);で、一時変数n1に格納されていたチャネルのポインタを、新しく割り当てられたレジスタn2に移動します。そして、n1 = n2;n1がこのレジスタを指すように更新されます。

この一連の変更により、cap(CHAN)の計算に必要なチャネルポインタが、不必要に早い段階でレジスタに固定されることがなくなります。代わりに、一時変数として扱われ、必要な時にレジスタにロードされるようになります。これにより、レジスタの利用効率が向上し、コンパイラがより柔軟にレジスタを管理できるようになるため、全体としてより最適化されたアセンブリコードが生成されることが期待されます。

また、test/torture.goに新しいテストケースChainCap()が追加されています。これは、ネストされたcap(make(chan int, cap(...)))の呼び出しを含むselectステートメントをテストするもので、この変更が複雑なチャネル操作のコード生成に悪影響を与えないことを確認するためのものです。特に、cap()が複数回、かつネストして呼び出されるようなエッジケースでのコンパイラの挙動を検証しています。

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

diff --git a/src/cmd/8g/cgen.c b/src/cmd/8g/cgen.c
index d626c2eb02..5988a4328c 100644
--- a/src/cmd/8g/cgen.c
+++ b/src/cmd/8g/cgen.c
@@ -347,8 +347,11 @@ cgen(Node *n, Node *res)\n 		if(istype(nl->type, TCHAN)) {\n 			// chan has cap in the second 32-bit word.\n 			// a zero pointer means zero length\n-\t\t\tregalloc(&n1, types[tptr], res);\n+\t\t\ttempname(&n1, types[tptr]);\n \t\t\tcgen(nl, &n1);\n+\t\t\tregalloc(&n2, types[tptr], N);\n+\t\t\tgmove(&n1, &n2);\n+\t\t\tn1 = n2;\n \n \t\t\tnodconst(&n2, types[tptr], 0);\n \t\t\tgins(optoas(OCMP, types[tptr]), &n1, &n2);\ndiff --git a/test/torture.go b/test/torture.go
index bbf6d347d9..197b481e66 100644
--- a/test/torture.go
+++ b/test/torture.go
@@ -337,3 +337,10 @@ func ChainDivConst(a int) int {\n func ChainMulBytes(a, b, c byte) byte {\n 	return a*(a*(a*(a*(a*(a*(a*(a*(a*b+c)+c)+c)+c)+c)+c)+c)+c) + c\n }\n+\n+func ChainCap() {\n+\tselect {\n+\tcase <-make(chan int, cap(make(chan int, cap(make(chan int, cap(make(chan int, cap(make(chan int))))))))):\n+\tdefault:\n+\t}\n+}\n

コアとなるコードの解説

src/cmd/8g/cgen.c の変更

このファイルは、Goコンパイラのコードジェネレータの一部であり、抽象構文木(AST)のノードをターゲットアーキテクチャ(この場合は386)のアセンブリコードに変換する役割を担っています。

変更はcgen関数内で行われています。この関数は、Goの式やステートメントに対応するアセンブリコードを生成する主要な関数です。特に、cap()len()のような組み込み関数の処理に関連する部分です。

元のコード:

			regalloc(&n1, types[tptr], res);
			cgen(nl, &n1);

ここでは、n1というノード(チャネルのポインタを表す)に対して、res(結果を格納する場所)を考慮してレジスタを早期に割り当てようとしていました。cgen(nl, &n1)は、チャネルのポインタ自体をn1に生成する処理です。

変更後のコード:

			tempname(&n1, types[tptr]);
			cgen(nl, &n1);
			regalloc(&n2, types[tptr], N);
			gmove(&n1, &n2);
			n1 = n2;
  1. tempname(&n1, types[tptr]);: n1を一時的な変数として宣言し、その型をポインタ型(types[tptr])に設定します。この時点では、まだ物理的なレジスタは割り当てられず、コンパイラの内部表現で一時的な記憶場所が確保されるだけです。
  2. cgen(nl, &n1);: チャネルのポインタ自体をn1に生成します。この結果は、まだレジスタではなく、一時的な記憶場所に格納されます。
  3. regalloc(&n2, types[tptr], N);: n2という新しいノードに対してレジスタを割り当てます。ここでNが指定されているのは、特定のレジスタを要求するのではなく、コンパイラに最適なレジスタを選択させることを意味します。これにより、レジスタ割り当ての柔軟性が高まります。
  4. gmove(&n1, &n2);: 一時変数n1に格納されていたチャネルのポインタを、新しく割り当てられたレジスタn2に移動します。
  5. n1 = n2;: n1がこのレジスタを指すように更新されます。これにより、以降の処理でn1が参照される際には、レジスタn2の値が使用されるようになります。

この変更のポイントは、チャネルのポインタをすぐにレジスタに割り当てるのではなく、一度一時変数として扱い、その後に最適なタイミングでレジスタにロードするようにした点です。これにより、レジスタの利用効率が向上し、コンパイラがより柔軟にレジスタを管理できるようになります。特に、cap()の計算が複雑な式の一部である場合や、チャネルポインタがすぐに使用されない場合に、レジスタの競合を減らし、より効率的なコード生成に貢献します。

test/torture.go の変更

このファイルは、Goコンパイラのストレステストやエッジケースのテストに使用されるものです。

追加されたテストケース:

func ChainCap() {
	select {
	case <-make(chan int, cap(make(chan int, cap(make(chan int, cap(make(chan int, cap(make(chan int))))))))):
	default:
	}
}

このChainCap関数は、非常に深くネストされたcap()呼び出しを含むmake(chan int, ...)式をselectステートメントのケースとして使用しています。このような複雑な式は、コンパイラのコード生成ロジックにとって挑戦的なシナリオとなります。このテストケースを追加することで、今回のcap()に関するコード生成の変更が、このような複雑な状況下でも正しく機能し、コンパイラがクラッシュしたり、誤ったコードを生成したりしないことを検証しています。特に、cap()が複数回、かつネストして呼び出されるようなエッジケースでのコンパイラの挙動を検証し、変更の堅牢性を保証する役割を果たします。

関連リンク

参考にした情報源リンク