[インデックス 10774] ファイルの概要
このコミットは、Goコンパイラのバックエンドの一部であるcmd/8c(x86/amd64アーキテクチャ向けコンパイラ)内のswt.cとpgen.cの2つのファイルを変更しています。
src/cmd/8c/swt.c: このファイルは、Go言語のswitch文のコンパイル、特にケース値の比較と分岐ロジックの生成を担当しています。switch文の最適化や、異なるデータ型(特に整数型)の扱いに関するコードが含まれています。src/cmd/cc/pgen.c: このファイルは、Goコンパイラのフロントエンドとバックエンドの間の共通コード生成部分の一部であり、式の評価や型チェック、そしてswitch文のような制御フロー構造の初期処理に関わっています。
コミット
このコミットは、Goコンパイラ(8c)が64ビットのswitch式を扱えるようにするための変更です。これまでは32ビット値に限定されていましたが、この変更により、switch文の対象となる値が64ビット整数型(int64やuint64)であっても正しくコンパイルされるようになります。ただし、caseラベル自体は引き続き32ビット値に制限されており、これは将来の改善点として残されています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6481e37d28b61bfae99a6fe2f70fa0412da16de6
元コミット内容
commit 6481e37d28b61bfae99a6fe2f70fa0412da16de6
Author: Russ Cox <rsc@golang.org>
Date: Wed Dec 14 00:08:38 2011 -0500
8c: handle 64-bit switch value
Cases must still be 32-bit values, but one thing at a time.
R=ality, ken2, ken
CC=golang-dev
https://golang.org/cl/5485063
変更の背景
Go言語は、当初からクロスプラットフォーム対応を念頭に置いて設計されており、32ビットおよび64ビットシステムの両方で動作します。しかし、コンパイラの初期の実装では、switch文の式が32ビット整数値に限定されていました。これは、特に64ビットシステムでint64やuint64のような大きな整数型をswitch文の式として使用しようとした場合に、コンパイルエラーや予期せぬ動作を引き起こす可能性がありました。
このコミットの背景には、Go言語の表現力を高め、より広範なユースケースに対応できるようにするという目的があります。64ビット整数は、データベースのID、タイムスタンプ、ハッシュ値など、多くのアプリケーションで頻繁に使用されます。これらの値をswitch文で直接扱えるようにすることで、開発者はより自然で効率的なコードを書くことができるようになります。
コミットメッセージにある「Cases must still be 32-bit values, but one thing at a time.」という記述は、この変更が64ビットswitch値のサポートに向けた最初の一歩であり、caseラベルの64ビット対応は将来の課題として残されていることを示しています。これは、コンパイラの変更が複雑であり、段階的に機能を追加していくという開発アプローチを反映しています。
前提知識の解説
Go言語のswitch文
Go言語のswitch文は、他のC系の言語と同様に、式の値に基づいて複数のコードブロックの中から一つを実行するための制御構造です。Goのswitchは、caseに複数の値を指定できたり、fallthroughキーワードで次のcaseに処理を継続させたり、式を省略してif-else ifのように使えたりするなど、柔軟な機能を持っています。
package main
import "fmt"
func main() {
i := 2
switch i {
case 1:
fmt.Println("one")
case 2, 3: // 複数の値を指定可能
fmt.Println("two or three")
default:
fmt.Println("other")
}
// 式を省略したswitch (type switchやif-else ifの代わり)
var x interface{} = "hello"
switch v := x.(type) {
case int:
fmt.Println("int:", v)
case string:
fmt.Println("string:", v)
default:
fmt.Println("unknown")
}
}
Goコンパイラの構造とcmd/8c
Goコンパイラは、複数のステージとコンポーネントから構成されています。大まかには以下のようになります。
- フロントエンド: ソースコードの字句解析、構文解析、抽象構文木(AST)の生成、型チェックなどを行います。
- ミドルエンド: ASTを最適化し、中間表現(IR)に変換します。
- バックエンド: 中間表現をターゲットアーキテクチャの機械語に変換します。
cmd/8cは、Goコンパイラのバックエンドの一部であり、x86-64(AMD64)アーキテクチャ向けのコード生成を担当します。Goコンパイラは、ターゲットアーキテクチャごとに異なるバックエンド(例: cmd/8c for amd64, cmd/6g for arm64, cmd/5g for arm)を持っています。8cという名前は、歴史的にIntel 8086プロセッサファミリーに由来しています。
コンパイラにおける型表現(TLONG, TVLONGなど)
コンパイラ内部では、Go言語の型(int, int64, stringなど)は、コンパイラ独自の内部表現にマッピングされます。このコミットに関連する部分では、整数型が重要です。
TLONG: 32ビット整数型を表すコンパイラ内部の型定数である可能性が高いです。TVLONG: 64ビット整数型を表すコンパイラ内部の型定数である可能性が高いです。Vは"Value"や"Vector"など、より大きな値を意味する接頭辞として使われることがあります。
これらの内部型定数は、コード生成時に適切なレジスタサイズや命令を選択するために使用されます。
コード生成の概念
コード生成は、コンパイラの最終段階であり、中間表現をターゲットプロセッサが実行できる機械語命令に変換するプロセスです。これには以下のステップが含まれます。
- レジスタ割り当て: 変数や中間結果をCPUのレジスタに割り当てます。レジスタは高速ですが数が限られているため、効率的な割り当てが重要です。
- 命令選択: 中間表現の操作に対応する機械語命令を選択します。
- 命令スケジューリング: 命令の実行順序を最適化し、パイプラインの効率を最大化します。
- 分岐とジャンプ:
if文やswitch文などの制御フローを、条件分岐命令やジャンプ命令に変換します。
gopcodeとboolgen
これらはGoコンパイラの内部関数であり、コード生成の特定の側面を担当します。
gopcode(op, type, left, right): 特定の操作(op、例:OEQ(等価),OGT(より大きい))に対応する機械語命令を生成するための汎用関数である可能性が高いです。typeはオペランドの型、leftとrightはオペランドを表すノードです。boolgen(node, true_label, false_label): ブール式(例:a == b,x > y)を評価し、その結果に基づいて条件分岐命令を生成する関数である可能性が高いです。true_labelとfalse_labelは、式が真または偽の場合にジャンプするターゲットのアドレスを示します。64ビット値の比較は、32ビット値の比較よりも複雑になるため、boolgenのようなより抽象的な関数が導入されたと考えられます。
技術的詳細
このコミットの技術的な核心は、switch文の式が64ビット値である場合に、コンパイラがそれを正しく処理し、適切な比較命令を生成するようにswt.cとpgen.cを変更した点にあります。
src/cmd/8c/swt.cの変更点
swt.cのswit1関数は、switch文の各caseを処理する主要なロジックを含んでいます。
-
64ビット値の検出と特殊処理: 変更前は、
switch式の型に関わらず一律に32ビット値として扱われていました。変更後、if(typev[n->type->etype])という条件が追加され、n(switch式を表すノード)の型が64ビット整数型(TVLONG)であるかをチェックします。- もし64ビット型であれば、
regsalloc(64ビットレジスタの割り当て)とnreg.type = types[TVLONG](型を64ビットに設定)が行われ、cgen(n, &nreg)で64ビット値としてレジスタにロードされます。その後、再帰的にswit1が呼び出され、この64ビット値が処理されます。 - 32ビット型の場合は、
regalloc(32ビットレジスタの割り当て)とnreg.type = types[TLONG](型を32ビットに設定)が行われ、同様にcgenでレジスタにロードされます。
- もし64ビット型であれば、
-
比較ロジックの変更:
switch文の各case値との比較(OEQ)や、範囲チェック(OGT)のロジックが変更されました。- 変更前は、
gopcode(OEQ, n->type, n, nodconst(q->val))のように、gopcode関数で直接比較命令を生成していました。これは32ビット値の比較には適していましたが、64ビット値の比較には不十分でした。 - 変更後、再び
if(n->type && typev[n->type->etype])で64ビット型であるかをチェックします。- 64ビット型の場合、
memset(&n1, 0, sizeof n1); n1.op = OEQ; n1.left = n; n1.right = &ncon; boolgen(&n1, 1, Z);のように、一時的なNode構造体n1を作成し、比較操作(OEQまたはOGT)を設定した後、boolgen関数を呼び出しています。boolgenは、より複雑なブール式(ここでは64ビット値の比較)を評価し、適切な条件分岐命令を生成するために使用されます。これは、64ビット値の比較が複数の命令を必要とする場合があるため、より抽象的なboolgenに処理を委譲することで、コード生成の複雑さを隠蔽しています。 - 32ビット型の場合は、引き続き
gopcodeが使用されます。
- 64ビット型の場合、
- 変更前は、
src/cmd/cc/pgen.cの変更点
pgen.cの変更は、switch文の式の型チェックと、doswit関数の呼び出し方法の簡素化に焦点を当てています。
-
型チェックの変更:
if(!typeword[l->type->etype] || l->type->etype == TIND)がif(!typechlvp[l->type->etype] || l->type->etype == TIND)に変更されました。typewordは、おそらく「ワードサイズ(32ビット)に収まる型」をチェックするフラグでした。typechlvpは、より汎用的な「文字、ハーフワード、ロング、ポインタ型」をチェックするフラグである可能性があり、これにより64ビット整数型もswitch式の有効な型として認識されるようになります。この変更は、switch式の型が整数型であることを確認するためのチェックを、より広範な整数型に対応できるように更新したことを意味します。
-
doswit関数の呼び出しの簡素化: 変更前は、switch式のレジスタ割り当て、コード生成、レジスタ解放をpgen.c内で明示的に行っていました。regalloc(&nod, l, Z); if(typev[l->type->etype]) nod.type = types[TVLONG]; else nod.type = types[TLONG]; cgen(l, &nod); doswit(&nod); regfree(&nod);これが、
doswit(l);という単一の呼び出しに置き換えられました。- この変更は、
doswit関数が内部でswitch式のレジスタ割り当て、コード生成、およびレジスタ解放のロジックをカプセル化するようになったことを示唆しています。これにより、pgen.cはswitch式の詳細な処理から解放され、コードのモジュール化と保守性が向上します。また、doswitが64ビット値の処理を内部で適切にハンドリングするようになったため、呼び出し側で型の違いを意識する必要がなくなりました。
- この変更は、
これらの変更により、Goコンパイラは64ビット整数をswitch式の値として受け入れ、それらを効率的かつ正確に機械語に変換できるようになりました。
コアとなるコードの変更箇所
src/cmd/8c/swt.c
--- a/src/cmd/8c/swt.c
+++ b/src/cmd/8c/swt.c
@@ -36,12 +36,40 @@ swit1(C1 *q, int nc, int32 def, Node *n)
C1 *r;
int i;
Prog *sp;
+ Node n1, nreg, ncon;
+
+ if(typev[n->type->etype]) {
+ if(n->op != ONAME || n->sym != nodsafe->sym) {
+ regsalloc(&nreg, n);
+ nreg.type = types[TVLONG];
+ cgen(n, &nreg);
+ swit1(q, nc, def, &nreg);
+ return;
+ }
+ } else {
+ if(n->op != OREGISTER) {
+ regalloc(&nreg, n, Z);
+ nreg.type = types[TLONG];
+ cgen(n, &nreg);
+ swit1(q, nc, def, &nreg);
+ regfree(&nreg);
+ return;
+ }
+ }
if(nc < 5) {
for(i=0; i<nc; i++) {
if(debug['W'])
print("case = %.8ux\\n", q->val);
- gopcode(OEQ, n->type, n, nodconst(q->val));
+ if(n->type && typev[n->type->etype]) {
+ memset(&n1, 0, sizeof n1);
+ n1.op = OEQ;
+ n1.left = n;
+ ncon = *nodconst(q->val);
+ n1.right = &ncon;
+ boolgen(&n1, 1, Z);
+ } else
+ gopcode(OEQ, n->type, n, nodconst(q->val));
patch(p, q->label);
q++;
}
@@ -53,10 +81,22 @@ swit1(C1 *q, int nc, int32 def, Node *n)
r = q+i;
if(debug['W'])
print("case > %.8ux\\n", r->val);
- gopcode(OGT, n->type, n, nodconst(r->val));
- sp = p;
- gbranch(OGOTO);
- p->as = AJEQ;
+ if(n->type && typev[n->type->etype]) {
+ memset(&n1, 0, sizeof n1);
+ n1.op = OGT;
+ n1.left = n;
+ ncon = *nodconst(r->val);
+ n1.right = &ncon;
+ boolgen(&n1, 1, Z);
+ sp = p;
+ n1.op = OEQ;
+ boolgen(&n1, 1, Z);
+ } else {
+ gopcode(OGT, n->type, n, nodconst(r->val));
+ sp = p;
+ gbranch(OGOTO);
+ p->as = AJEQ;
+ }
patch(p, r->label);
swit1(q, i, def, n);
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;
コアとなるコードの解説
src/cmd/8c/swt.cの変更解説
swit1関数は、switch文のコンパイルにおいて、switch式の値と各caseラベルの値を比較し、適切な分岐を生成する役割を担っています。
-
Node n1, nreg, ncon;の追加: これは、64ビット値を扱うための新しいノードやレジスタ情報を一時的に保持するための変数宣言です。nregはレジスタ割り当て用、n1は比較操作のASTノード構築用、nconは定数ノード用です。 -
if(typev[n->type->etype]) { ... } else { ... }ブロック: このブロックは、switch式の型が64ビット(typevが真)か32ビット(typevが偽)かに応じて、異なるレジスタ割り当てとコード生成パスを選択します。- 64ビットの場合:
regsalloc(&nreg, n); nreg.type = types[TVLONG]; cgen(n, &nreg); swit1(q, nc, def, &nreg); return;regsallocは、64ビット値を保持するためのレジスタを割り当てます。nreg.type = types[TVLONG]は、割り当てられたレジスタの型を64ビット整数型に設定します。cgen(n, &nreg)は、元のswitch式nの値を、新しく割り当てられた64ビットレジスタnregにロードする機械語を生成します。- その後、
swit1関数自身が再帰的に呼び出され、今度はレジスタにロードされた64ビット値&nregを対象として処理を続行します。これにより、swit1の残りの部分が、すでにレジスタに存在する64ビット値を扱うことができるようになります。
- 32ビットの場合:
regalloc(&nreg, n, Z); nreg.type = types[TLONG]; cgen(n, &nreg); swit1(q, nc, def, &nreg); regfree(&nreg); return;- 同様に、
regallocで32ビットレジスタを割り当て、TLONG型を設定し、値をロードします。処理後にはregfreeでレジスタを解放します。
- 同様に、
- 64ビットの場合:
-
gopcodeからboolgenへの切り替え(OEQとOGTの比較):switch文の各case値との等価比較(OEQ)や、範囲チェックのための「より大きい」比較(OGT)のロジックが変更されました。- 変更前:
gopcode(OEQ, n->type, n, nodconst(q->val));- これは、
nと定数q->valを比較する単純な機械語命令を生成していました。これは32ビット値には十分でした。
- これは、
- 変更後(64ビットの場合):
memset(&n1, 0, sizeof n1); n1.op = OEQ; // または OGT n1.left = n; ncon = *nodconst(q->val); n1.right = &ncon; boolgen(&n1, 1, Z);memset(&n1, 0, sizeof n1);でn1を初期化します。n1.op = OEQ;(またはOGT;) で、このノードが等価比較またはより大きい比較を表すことを示します。n1.left = n;とn1.right = &ncon;で、比較の左オペランド(switch式の値)と右オペランド(case定数)を設定します。nconはnodconst(q->val)で作成された定数ノードのコピーです。boolgen(&n1, 1, Z);は、構築されたブール式n1を評価し、その結果に基づいて条件分岐命令を生成します。64ビット値の比較は、単一の命令で完結しない場合があるため、boolgenのようなより高レベルの関数に処理を委譲することで、コンパイラは複雑な命令シーケンスを適切に生成できます。
- 変更前:
src/cmd/cc/pgen.cの変更解説
pgen.cは、Goコンパイラの共通コード生成部分であり、switch文の初期処理とdoswit関数への委譲を担当します。
-
typewordからtypechlvpへの変更:if(!typeword[l->type->etype] || l->type->etype == TIND)がif(!typechlvp[l->type->etype] || l->type->etype == TIND)に変更されました。- この行は、
switch式の型が整数型であることを確認するための型チェックです。 typewordは、おそらく32ビットワードに収まる型を意味していました。typechlvpは、char,halfword,long,pointerといった、より広範な整数およびポインタ型をカバーするフラグです。この変更により、64ビット整数型もswitch式の有効な型として認識されるようになり、コンパイラがint64やuint64をswitch式として受け入れるための前提条件が整いました。
- この行は、
-
doswit(l);への簡素化: 変更前は、switch式lに対して、レジスタ割り当て(regalloc)、型設定(nod.type = types[TVLONG]またはTLONG)、コード生成(cgen)、そしてdoswitの呼び出し、レジスタ解放(regfree)という一連の処理を明示的に行っていました。- 変更後: これらすべての処理が
doswit(l);という単一の呼び出しに集約されました。 - これは、
doswit関数が内部でswitch式の型を判断し、適切なレジスタ割り当て、コード生成、およびレジスタ解放を行うようにリファクタリングされたことを意味します。これにより、pgen.cのコードはより簡潔になり、switch文のコンパイルロジックがdoswit関数内にカプセル化され、モジュール性が向上しました。また、doswitが64ビット値の処理を透過的に行えるようになったため、呼び出し側は型の違いを意識する必要がなくなりました。
- 変更後: これらすべての処理が
これらの変更は、Goコンパイラが64ビット整数をswitch式の値として効率的かつ正確に処理できるようにするための重要なステップです。
関連リンク
- Go言語公式ドキュメント: https://go.dev/
- Go言語の
switch文に関する公式ドキュメント: https://go.dev/tour/flowcontrol/9 - Goコンパイラのソースコード(GitHub): https://github.com/golang/go
- Go Gerrit (ChangeList 5485063): https://golang.org/cl/5485063
参考にした情報源リンク
- Go言語のコンパイラに関する一般的な情報源(Goのコンパイラがどのように動作するかを理解するために使用)
- Go言語の
switch文の動作に関する情報源 - C言語のコンパイラ設計に関する一般的な知識(
gopcode,boolgenなどの概念を理解するために使用) - x86-64アーキテクチャのレジスタと命令セットに関する情報(64ビット値の処理の背景を理解するために使用)
- Go言語のソースコード内のコメントや関連するコミット履歴(より深い理解のために参照)
- Go言語のIssueトラッカーやメーリングリストの議論(機能追加の背景や課題を理解するために参照)