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

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

このコミットは、Goコンパイラ(gc)のバックエンド部分であるsrc/cmd/gc/plive.csrc/cmd/gc/popt.cに対する変更です。

  • src/cmd/gc/plive.c: 主にライブネス解析(liveness analysis)に関連するコードが含まれています。ライブネス解析は、プログラムの特定のポイントでどの変数が「生きている」(将来使用される可能性がある)かを判断するコンパイラの最適化フェーズの一部です。これにより、不要な変数のメモリを解放したり、レジスタ割り当てを最適化したりできます。
  • src/cmd/gc/popt.c: 主にプログラムの最適化(optimization)に関連するコードが含まれています。これには、ジャンプ命令の最適化や不要なコードの削除などが含まれる可能性があります。

これらのファイルは、Goプログラムがコンパイルされて実行可能バイナリになる過程で、中間表現(IR)に対する変換や最適化を行う重要な役割を担っています。

コミット

このコミットは、特定のコンパイラ環境、特にchar型がデフォルトでunsigned charとして扱われるコンパイラでGoコンパイラgcをビルドする際の問題を修正します。具体的には、plive.cpopt.c内のmodeフィールドの比較値を-1から1に変更することで、ビルドエラーを解消しています。

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

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

元コミット内容

commit f0023cf1d57aabd8c0aa30a65eef9ec7de041eb6
Author: Russ Cox <rsc@golang.org>
Date:   Fri Feb 14 00:43:43 2014 -0500

    cmd/gc: fix build for 'default unsigned char' compilers
    
    TBR=iant
    CC=golang-codereviews
    https://golang.org/cl/63680045
---
 src/cmd/gc/plive.c | 2 +-\
 src/cmd/gc/popt.c  | 2 +-\
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/cmd/gc/plive.c b/src/cmd/gc/plive.c
index d353672985..250d9236b3 100644
--- a/src/cmd/gc/plive.c
+++ b/src/cmd/gc/plive.c
@@ -521,7 +521,7 @@ newcfg(Prog *firstp)
 
 			// Stop before an unreachable RET, to avoid creating
 			// unreachable control flow nodes.
-			if(p->link != nil && p->link->as == ARET && p->link->mode == -1)
+			if(p->link != nil && p->link->as == ARET && p->link->mode == 1)
 				break;
 
 			// Collect basic blocks with selectgo calls.
diff --git a/src/cmd/gc/popt.c b/src/cmd/gc/popt.c
index 1f0bdb496a..f5067bd0e1 100644
--- a/src/cmd/gc/popt.c
+++ b/src/cmd/gc/popt.c
@@ -155,7 +155,7 @@ fixjmp(Prog *firstp)
 			// this assumption will not hold in the case of an infinite loop
 			// at the end of a function.
 			// Keep the RET but mark it dead for the liveness analysis.
-			p->mode = -1;
+			p->mode = 1;
 			} else {
 			if(debug['R'] && debug['v'])
 				print("del %P\n", p);

変更の背景

このコミットの背景には、C言語におけるchar型の符号付き/符号なしのデフォルトの扱いの違いが関係しています。C標準では、char型がsigned charとして扱われるかunsigned charとして扱われるかは実装定義(implementation-defined)です。ほとんどのシステムではsigned charがデフォルトですが、一部のコンパイラ(特にARMアーキテクチャ向けのコンパイラなど)ではunsigned charがデフォルトとなることがあります。

GoコンパイラgcはC言語で書かれており、その内部でchar型や他の整数型を広範囲に使用しています。このコミットで修正された問題は、p->modeというフィールドが、特定の状態を示すために-1という値を保持している箇所に関連しています。

charがデフォルトでunsignedであるコンパイラ環境では、-1という値がchar型に代入されると、それはunsigned charの最大値(通常は255)として解釈されます。これは、符号付き整数としての-1とは異なる値です。Goコンパイラのコード内で、このmodeフィールドが-1であるかどうかをチェックするロジックがあった場合、unsigned charがデフォルトのコンパイラでは、このチェックが常に失敗し、予期せぬ動作やビルドエラーを引き起こす可能性がありました。

この問題は、Goコンパイラ自体の移植性に関わるものであり、異なるCコンパイラ環境でGoをビルドできるようにするために修正が必要でした。

前提知識の解説

C言語におけるchar型の符号付き/符号なし

C言語では、char型は文字を格納するために使用されますが、整数型としても扱えます。char型にはsigned charunsigned charの2つのバリエーションがあります。

  • signed char: 符号付き整数として扱われ、通常は-128から127までの値を表現できます。
  • unsigned char: 符号なし整数として扱われ、通常は0から255までの値を表現できます。

C標準では、プレーンなchar型がsigned charとして扱われるかunsigned charとして扱われるかは、コンパイラの実装に依存すると規定されています。多くのx86ベースのシステムではsigned charがデフォルトですが、ARMなどの組み込みシステム向けのコンパイラではunsigned charがデフォルトとなることがあります。

この違いは、特に-1のような負の値をchar型に代入し、その値を比較する際に問題を引き起こします。signed charでは-1-1ですが、unsigned charでは-1は2の補数表現によってUCHAR_MAX(通常は255)として解釈されます。

Goコンパイラ gc

Go言語の公式コンパイラはgcと呼ばれ、主にC言語で書かれています(一部はGo言語で書かれています)。gcは、Goのソースコードを機械語に変換する役割を担っています。コンパイルプロセスには、構文解析、型チェック、中間コード生成、最適化、コード生成などの複数のフェーズがあります。

プログラムの制御フローとライブネス解析

コンパイラは、プログラムの実行パスを分析するために「制御フローグラフ(Control Flow Graph, CFG)」を構築します。CFGは、プログラムの基本ブロック(連続した命令のシーケンス)をノードとし、ブロック間のジャンプや条件分岐をエッジとするグラフです。

ライブネス解析は、データフロー解析の一種で、CFG上で変数が「生きている」かどうかを判断します。ある変数がプログラムの特定のポイントで生きているとは、その変数の現在の値が将来のどこかの時点で読み取られる可能性があることを意味します。ライブネス解析の結果は、レジスタ割り当ての最適化や、不要なコードの削除(デッドコードエリミネーション)などに利用されます。

Prog構造体とmodeフィールド

Goコンパイラの内部では、プログラムの命令や操作はProgという構造体で表現されることがあります。このProg構造体には、命令の種類(asフィールド)、リンク先の命令(linkフィールド)、そして様々なフラグや状態を示すためのmodeフィールドなどが含まれています。

このコミットでは、modeフィールドが特定の状態を示すために使用されており、その値が-1であるかどうかをチェックしていました。

技術的詳細

このコミットの技術的な核心は、C言語のchar型の符号付き/符号なしのデフォルトの扱いの違いによって、Goコンパイラgcの内部ロジックが誤動作していた点にあります。

Goコンパイラのsrc/cmd/gc/plive.csrc/cmd/gc/popt.cでは、Prog構造体のmodeフィールドが、特定の命令の状態や特性を示すために使用されていました。元のコードでは、このmodeフィールドに-1という値を設定したり、-1と比較したりしていました。

例えば、popt.cfixjmp関数では、到達不能なRET(return命令)をライブネス解析から除外するために、p->mode = -1;としていました。これは、このRET命令が「デッド」(不要)であることを示すフラグとして-1を使用していたと考えられます。

しかし、charがデフォルトでunsignedであるコンパイラでコンパイルされた場合、C言語の型変換規則により、-1というリテラル値がunsigned char型の変数に代入されると、それはunsigned charの最大値(例えば255)として解釈されます。

  • signed charの場合: -1はそのまま-1
  • unsigned charの場合: -10xFF(255)として解釈される。

したがって、plive.cnewcfg関数内のif(p->link != nil && p->link->as == ARET && p->link->mode == -1)という条件式は、unsigned charがデフォルトのコンパイラでは、p->link->modeが実際に-1(符号付きとして)であっても、それがunsigned charとして255と解釈されるため、-1との比較が常にfalseとなり、条件が満たされなくなっていました。これにより、到達不能なRET命令が正しく処理されず、コンパイラのロジックに不整合が生じ、ビルドエラーや誤った最適化につながっていた可能性があります。

このコミットでは、modeフィールドの値を-1から1に変更することで、この問題を回避しています。つまり、modeフィールドが示す状態のセマンティクスを-1から1に切り替えたということです。1という値は、signed charでもunsigned charでも同じ1として解釈されるため、コンパイラのchar型のデフォルトの扱いに依存しない、より堅牢なコードになります。

この変更は、Goコンパイラがより多くのCコンパイラ環境で正しくビルドできるようにするための、移植性に関する修正です。

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

src/cmd/gc/plive.c

--- a/src/cmd/gc/plive.c
+++ b/src/cmd/gc/plive.c
@@ -521,7 +521,7 @@ newcfg(Prog *firstp)
 
 			// Stop before an unreachable RET, to avoid creating
 			// unreachable control flow nodes.
-			if(p->link != nil && p->link->as == ARET && p->link->mode == -1)
+			if(p->link != nil && p->link->as == ARET && p->link->mode == 1)
 				break;
 
 			// Collect basic blocks with selectgo calls.

src/cmd/gc/popt.c

--- a/src/cmd/gc/popt.c
+++ b/src/cmd/gc/popt.c
@@ -155,7 +155,7 @@ fixjmp(Prog *firstp)
 			// this assumption will not hold in the case of an infinite loop
 			// at the end of a function.
 			// Keep the RET but mark it dead for the liveness analysis.
-			p->mode = -1;
+			p->mode = 1;
 			} else {
 			if(debug['R'] && debug['v'])
 				print("del %P\n", p);

コアとなるコードの解説

src/cmd/gc/plive.cの変更

newcfg関数は、制御フローグラフを構築する際に使用されると考えられます。変更された行は、到達不能なRET命令(関数からの戻り命令)を検出して、それ以上制御フローノードを作成しないようにするための条件式です。

  • 変更前: if(p->link != nil && p->link->as == ARET && p->link->mode == -1)
    • この条件は、次の命令がARET(return命令)であり、かつそのmodeフィールドが-1である場合に真となります。-1は、このRET命令がライブネス解析において「デッド」または「到達不能」としてマークされていることを示していました。
  • 変更後: if(p->link != nil && p->link->as == ARET && p->link->mode == 1)
    • p->link->mode == -1p->link->mode == 1に変更されました。これにより、modeフィールドが1である場合に、この条件が真となるようにロジックが変更されました。これは、popt.cmode1に設定する変更と対応しています。

src/cmd/gc/popt.cの変更

fixjmp関数は、ジャンプ命令の最適化や不要なコードの処理を行うと考えられます。変更された行は、無限ループの終端にあるRET命令をライブネス解析のために「デッド」としてマークする部分です。

  • 変更前: p->mode = -1;
    • Prog構造体のmodeフィールドに-1を代入していました。これは、この命令がライブネス解析において無視されるべきであることを示していました。
  • 変更後: p->mode = 1;
    • p->mode = -1p->mode = 1に変更されました。これにより、modeフィールドに1を代入することで、同じくライブネス解析において無視されるべき状態を示すように変更されました。

これらの変更は、modeフィールドが示す「デッド」または「到達不能」という状態の表現方法を-1から1に統一することで、Cコンパイラのchar型のデフォルトの符号付き/符号なしの扱いの違いによる問題を解消しています。1という値は、符号付きでも符号なしでも同じ値として解釈されるため、より移植性の高いコードになります。

関連リンク

参考にした情報源リンク

  • C言語のchar型の符号付き/符号なしのデフォルトの扱いに関する情報 (例: C Standard, GCC documentation on char signedness)
  • Goコンパイラgcの内部構造に関する一般的な情報 (例: Goのソースコード、Goのコンパイラに関するブログ記事やドキュメント)
  • コンパイラの最適化、特にライブネス解析に関する一般的な情報 (例: コンパイラ設計の教科書、オンラインのコンパイラ理論に関する資料)
  • Go言語のGitHubリポジトリ (コミット履歴の確認)