[インデックス 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トラッカーやメーリングリストの議論(機能追加の背景や課題を理解するために参照)