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

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

このコミットは、Go言語の初期のコンパイラである5c (ARM), 6c (x86-64), 8c (x86) において、switch文が64ビットの値を適切に扱えるようにするための変更を導入しています。具体的には、switch文の評価対象となる値が64ビット整数型(long longint64など)である場合に、コンパイラが正しくコードを生成できるように、スイッチ処理のロジックが修正されています。

コミット

commit d89b7173c2e2c919677753f38ed67022ebea175d
Author: Anthony Martin <ality@pbrane.org>
Date:   Wed Dec 14 17:30:40 2011 -0500

    5c, 6c, 8c: support 64-bit switch value
    
    For real this time. :-)
    
    R=rsc, ken
    CC=golang-dev
    https://golang.org/cl/5486061

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

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

元コミット内容

このコミットの元の内容は、Go言語の初期のコンパイラ(5c, 6c, 8c)が、switch文で64ビットの値をサポートするように修正することです。コミットメッセージには「For real this time. :-)」とあり、以前にも同様の試みがあったものの、今回で完全に実装されたことを示唆しています。

変更の背景

Go言語は、その設計当初から様々なアーキテクチャをサポートすることを目指していました。switch文はプログラミングにおいて非常に一般的な制御構造であり、その評価対象となる値の型は、言語の表現力と実用性において重要です。特に、64ビットシステムが普及する中で、64ビット整数値をswitch文で直接扱うことができないのは大きな制約となります。

このコミットが行われた2011年12月は、Go言語がまだ比較的新しい時期であり、コンパイラやランタイムの基盤が活発に開発されていました。初期のGoコンパイラは、Plan 9のCコンパイラツールチェーン(5c, 6c, 8c)をベースにしており、これらのコンパイラはGo言語のコードを機械語に変換する役割を担っていました。

switch文が32ビット値のみをサポートしていた場合、開発者は64ビット値を扱う際に、明示的な型変換を行ったり、if-else ifの連鎖を使用したりするなど、不便な回避策を講じる必要がありました。これはコードの可読性やパフォーマンスに悪影響を与える可能性がありました。したがって、64ビット値のswitchサポートは、Go言語の実用性と表現力を向上させる上で不可欠な機能でした。

前提知識の解説

  • 5c, 6c, 8c コンパイラ: これらは、Go言語の初期のコンパイラツールチェーンの一部であり、Plan 9オペレーティングシステムのCコンパイラに由来します。

    • 5c: ARMアーキテクチャ(特にARM v5以降の32ビット)向けのCコンパイラ。
    • 6c: AMD64 (x86-64) アーキテクチャ向けのCコンパイラ。
    • 8c: Intel 386 (x86) アーキテクチャ向けのCコンパイラ。 Go 1.5以降、Goコンパイラ自体がGo言語で書かれるようになり(セルフホスティング)、これらのCコンパイラはGoのメインツールチェーンからは直接使用されなくなりましたが、Goの初期の発展において重要な役割を果たしました。
  • switch: プログラミング言語における制御構造の一つで、式の値に基づいて複数のコードブロックの中から一つを実行します。多くの言語では、switch文の評価対象は整数型や列挙型に限定されることがありますが、Go言語のswitch文はより柔軟で、任意の型の式を評価できます。

  • 64ビット値: 64ビットの幅を持つ整数値です。32ビット値と比較して、より大きな数値を表現できます(約9×10^18まで)。現代の多くのシステムは64ビットアーキテクチャであり、メモリのアドレス指定や大規模な数値計算において64ビット値が広く利用されています。

  • gc.h: Goコンパイラの共通ヘッダーファイルの一つで、型定義、関数プロトタイプ、マクロなどが含まれています。コンパイラの各部分で共有される重要な宣言がここに集約されています。

  • swt.c: Goコンパイラのソースファイルの一つで、switch文のコード生成ロジックを実装しています。switch文の各caseを効率的に処理するためのジャンプテーブルや比較命令の生成を担当します。

  • pgen.c: Goコンパイラのソースファイルの一つで、パーサーが生成した抽象構文木(AST)から、より低レベルの中間表現(PCODE)を生成する役割を担っています。型チェックや式の評価順序の決定など、コード生成の前段階の処理が含まれます。

技術的詳細

このコミットの主要な変更点は、switch文の処理を担う関数swit1のロジックを修正し、新たにswit2関数を導入したことです。これにより、switch文の評価対象が64ビット整数型である場合に、適切な型でレジスタを割り当て、コードを生成できるようになりました。

具体的には、以下の変更が行われています。

  1. swit2関数の導入: src/cmd/5c/gc.h, src/cmd/6c/gc.h, src/cmd/8c/gc.h の各ヘッダーファイルに、void swit2(C1*, int, int32, Node*); というプロトタイプが追加されました。これは、swit2関数がswitch文の実際のコード生成ロジックを担うことを示しています。

  2. swit1関数の役割変更: src/cmd/5c/swt.c, src/cmd/6c/swt.c, src/cmd/8c/swt.c の各swt.cファイルにおいて、既存のswit1関数の実装が変更されました。

    • 変更前のswit1は、switch文のコード生成の主要なロジックを含んでいました。
    • 変更後のswit1は、まずswitch式の型をチェックし、それが64ビット型(typev[n->type->etype]が真の場合、TVLONG型)であれば、64ビットレジスタを割り当てます。そうでなければ、通常の32ビットレジスタ(TLONG型)を割り当てます。
    • その後、cgen関数を呼び出して式を評価し、結果を割り当てられたレジスタに格納します。
    • 最終的に、実際のスイッチ処理は新しく導入されたswit2関数に委譲されます。これにより、swit1は型の前処理とレジスタ割り当てに特化し、swit2が共通のスイッチ処理ロジックを扱うという役割分担がなされました。
  3. swit2関数でのスイッチ処理: swit2関数は、swit1から渡されたレジスタに格納された値と、caseラベルの値を比較し、適切なジャンプ命令を生成します。この関数内で、再帰的に自身を呼び出すことで、二分探索木のような効率的なスイッチ処理を実現しています。64ビット値の比較もこの関数内で適切に処理されます。

  4. src/cmd/cc/pgen.c の変更: pgen.cファイルでは、switch文の式に対する型チェックロジックが変更されました。

    • 変更前は、!typeword[l->type->etype] || l->type->etype == TIND という条件で、switch式が整数型であることを確認していました。typewordは32ビット整数型をチェックするものでした。
    • 変更後は、!typechlvp[l->type->etype] || l->type->etype == TIND という条件に変わっています。typechlvpは、文字型、短整数型、長整数型、および64ビット長整数型(long long)を含む、より広範な整数型をチェックするものです。これにより、switch式が64ビット整数型であってもエラーにならず、正しく処理されるようになります。
    • また、pgen.c内のdoswit関数の呼び出し部分も簡略化され、switch式のノードを直接doswitに渡すようになりました。レジスタ割り当てのロジックはswit1(そしてswit2)に移動したため、pgen.cからはその詳細が隠蔽されました。

これらの変更により、Goコンパイラはswitch文において64ビット整数値をネイティブにサポートできるようになり、より堅牢で表現力豊かなコードの生成が可能になりました。

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

このコミットで変更された主要なファイルとコードスニペットは以下の通りです。

1. src/cmd/5c/gc.h, src/cmd/6c/gc.h, src/cmd/8c/gc.h (ヘッダーファイル)

--- a/src/cmd/5c/gc.h
+++ b/src/cmd/5c/gc.h
@@ -304,6 +304,7 @@ void	gpseudo(int, Sym*, Node*);
 int	swcmp(const void*, const void*);
 void	doswit(Node*);
 void	swit1(C1*, int, int32, Node*);
+void	swit2(C1*, int, int32, Node*);
 void	newcase(void);
 void	bitload(Node*, Node*, Node*, Node*, Node*);
 void	bitstore(Node*, Node*, Node*, Node*, Node*);

swit2関数のプロトタイプが追加されています。

2. src/cmd/5c/swt.c, src/cmd/6c/swt.c, src/cmd/8c/swt.c (スイッチ処理の実装)

swit1関数の実装が変更され、swit2関数が新しく追加されています。

例: src/cmd/5c/swt.c

--- a/src/cmd/5c/swt.c
+++ b/src/cmd/5c/swt.c
@@ -28,11 +28,30 @@
 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 // THE SOFTWARE.
 
-
 #include "gc.h"
 
 void
 swit1(C1 *q, int nc, int32 def, Node *n)
+{
+	Node nreg;
+
+	if(typev[n->type->etype]) {
+		regsalloc(&nreg, n);
+		nreg.type = types[TVLONG];
+		cgen(n, &nreg);
+		swit2(q, nc, def, &nreg);
+		return;
+	}
+
+	regalloc(&nreg, n, Z);
+	nreg.type = types[TLONG];
+	cgen(n, &nreg);
+	swit2(q, nc, def, &nreg);
+	regfree(&nreg);
+}
+
+void
+swit2(C1 *q, int nc, int32 def, Node *n)
 {
 	C1 *r;
 	int i;
@@ -65,12 +84,12 @@ swit1(C1 *q, int nc, int32 def, Node *n)
 	sp = p;
 	gopcode(OEQ, nodconst(r->val), n, Z);	/* just gen the B.EQ */
 	patch(p, r->label);
-	swit1(q, i, def, n);
+	swit2(q, i, def, n);
 
 	if(debug['W'])
 		print("case < %.8ux\\n", r->val);
 	patch(sp, pc);
-	swit1(r+1, nc-i-1, def, n);
+	swit2(r+1, nc-i-1, def, n);
 	return;
 
 direct:

swit1swit2を呼び出すように変更され、swit2が実際のスイッチ処理ロジックを保持しています。typev[n->type->etype]のチェックにより、64ビット型の場合はTVLONG(64ビット長整数)を、それ以外はTLONG(32ビット長整数)を使用するように分岐しています。

3. src/cmd/cc/pgen.c (パーサーとコード生成の連携)

--- a/src/cmd/cc/pgen.c
+++ b/src/cmd/cc/pgen.c
@@ -293,7 +293,7 @@ loop:
 		complex(l);
 		if(l->type == T)
 			break;
-		if(!typeword[l->type->etype] || l->type->etype == TIND) {
+		if(!typechlvp[l->type->etype] || l->type->etype == TIND) {
 			diag(n, "switch expression must be integer");
 			break;
 		}
@@ -320,15 +320,7 @@ loop:
 		}
 
 		patch(sp, pc);
-		regalloc(&nod, l, Z);
-		/* always signed */
-		if(typev[l->type->etype])
-			nod.type = types[TVLONG];
-		else
-			nod.type = types[TLONG];
-		cgen(l, &nod);
-		doswit(&nod);
-		regfree(&nod);
+		doswit(l);
 		patch(spb, pc);
 
 		cases = cn;

typewordtypechlvpに変更され、switch式の型チェックがより広範な整数型に対応しました。また、doswitの呼び出しが簡略化され、レジスタ割り当てのロジックがswit1/swit2に移動したことがわかります。

コアとなるコードの解説

このコミットの核心は、swit1swit2という2つの関数によるswitch文の処理の分担と、64ビット値の適切な型処理です。

  • swit1関数: この関数は、switch文の式(Node *n)を受け取り、その式の型を検査します。 if(typev[n->type->etype]) の条件は、nの型が64ビット整数型(long longint64など)であるかどうかを判定します。

    • もし64ビット型であれば、regsalloc(&nreg, n); を使って64ビット値を格納できるレジスタ(nreg)を割り当て、nreg.type = types[TVLONG]; でそのレジスタの型をTVLONG(Type Value Long、64ビット長整数)に設定します。
    • そうでなければ、regalloc(&nreg, n, Z); を使って通常のレジスタを割り当て、nreg.type = types[TLONG]; でそのレジスタの型をTLONG(Type Long、32ビット長整数)に設定します。 その後、cgen(n, &nreg); を呼び出して、switch式の値を実際にnregに格納する機械語コードを生成します。 最後に、swit2(q, nc, def, &nreg); を呼び出し、実際のスイッチ処理をswit2に委譲します。swit1は、switch式の評価と、その結果を格納するレジスタの準備に特化しています。
  • swit2関数: この関数は、swit1から渡された、switch式の値が格納されたレジスタ(Node *n)と、caseラベルの情報(C1 *q, int nc, int32 def)を受け取ります。 swit2の内部では、switch文の各caseを効率的に処理するために、二分探索のようなロジックが実装されています。これは、caseラベルの値をソートし、中央のcase値と比較することで、ジャンプ先を絞り込む手法です。 gopcode(OEQ, nodconst(r->val), n, Z); のような行は、switch式の値(n)と現在のcase値(r->val)を比較し、等しい場合に指定されたラベル(r->label)にジャンプする機械語命令を生成します。 swit2は再帰的に自身を呼び出すことで、残りのcaseを処理します。この再帰的な構造により、多数のcaseを持つswitch文でも効率的なコードが生成されます。

pgen.cの変更は、switch文の式に対するコンパイラの型チェックを緩和し、64ビット整数型も有効なswitch式の型として認識するようにしました。これにより、フロントエンド(パーサー)からバックエンド(コード生成)への連携がスムーズになり、64ビットswitch値のサポートがエンドツーエンドで実現されました。

関連リンク

  • Go言語の公式ドキュメント: https://go.dev/
  • Go言語のswitch文に関するドキュメント: https://go.dev/ref/spec#Switch_statements
  • Go言語の初期のコンパイラに関する情報(Plan 9との関連など)は、Goの歴史に関する記事やGoのソースコードリポジトリの初期のコミット履歴で確認できます。

参考にした情報源リンク