[インデックス 17215] ファイルの概要
このコミットは、Goランタイムのproc.c
ファイルにおけるARMアーキテクチャでのビルド問題を修正するものです。具体的には、条件演算子(三項演算子 ? :
)の使用が原因で発生していた問題を、明示的なif
文に置き換えることで解決しています。
コミット
commit 1a09d70e230a7a9468ded587c7a1e3bee53c402b
Author: Russ Cox <rsc@golang.org>
Date: Tue Aug 13 19:37:54 2013 -0400
runtime: fix build on arm
Do not use ? :
I cannot say this enough.
TBR=dvyukov
CC=golang-dev
https://golang.org/cl/12903043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1a09d70e230a7a9468ded587c7a1e3bee53c402b
元コミット内容
runtime: fix build on arm
Do not use ? :
I cannot say this enough.
TBR=dvyukov
CC=golang-dev
https://golang.org/cl/12903043
変更の背景
このコミットの背景には、GoランタイムのC言語部分が特定のアーキテクチャ、特にARMでビルドする際に問題が発生していたことがあります。コミットメッセージの「Do not use ? : I cannot say this enough.」という強い表現から、条件演算子(三項演算子)の使用が、コンパイラの挙動や型推論、あるいはABI(Application Binary Interface)の差異によって、予期せぬビルドエラーや実行時エラーを引き起こす可能性があったことが示唆されます。
C言語の条件演算子 (condition ? value_if_true : value_if_false)
は、簡潔な記述が可能ですが、その結果の型は value_if_true
と value_if_false
の型に基づいて決定されます。もしこれらの型が異なる場合、C言語の型変換ルールに従って共通の型に昇格されます。この型昇格の挙動が、特定のコンパイラやアーキテクチャ(この場合はARM)において、期待通りの結果とならなかったり、コンパイルエラーを引き起こしたりすることがあります。特に、ポインタ型と整数型を混在させる場合や、異なるサイズの整数型を扱う場合に問題が生じやすいです。
Goランタイムは、パフォーマンスが重要となる部分や、OSとのインタラクションが必要な部分でC言語(またはPlan 9 C)を使用しています。そのため、C言語コードの移植性や堅牢性は非常に重要です。このコミットは、そのようなビルドの不安定性を解消し、ARMアーキテクチャでのGoの利用を確実にするためのものです。
前提知識の解説
- Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルのコンポーネントです。ガベージコレクション、スケジューリング、メモリ管理など、Go言語の並行性モデルを支える重要な機能を提供します。一部はC言語(またはPlan 9 C)で記述されています。
- M, P, G (Goroutine Scheduler): Goランタイムのスケジューラを構成する主要な要素です。
- M (Machine): OSのスレッドを表します。GoプログラムはM上で実行されます。
- P (Processor): 論理プロセッサを表し、MにGoルーチン(G)を実行するためのコンテキストを提供します。
- G (Goroutine): Go言語の軽量スレッドです。
- 条件演算子 (Ternary Operator)
? :
: C言語における三項演算子で、condition ? expression_if_true : expression_if_false
の形式を取ります。condition
が真であればexpression_if_true
が、偽であればexpression_if_false
が評価され、その結果が返されます。 - 型昇格 (Type Promotion): C言語において、異なる型のオペランドが演算子に与えられた場合、コンパイラが自動的にそれらを共通の型に変換するプロセスです。例えば、
int
とlong
の演算では、int
がlong
に昇格されることがあります。 - ARMアーキテクチャ: スマートフォン、タブレット、組み込みシステムなどで広く使用されているCPUアーキテクチャです。x86とは異なる命令セットやABIを持つため、特定のC言語コードが異なる挙動を示すことがあります。
runtime·printf
: Goランタイム内部で使用されるprintf
に似た関数です。デバッグ情報やトレース情報を出力するために使われます。
技術的詳細
このコミットの技術的な核心は、C言語の条件演算子 ? :
の使用を、より明示的な if
文に置き換えることで、ARMアーキテクチャでのビルド問題を回避した点にあります。
元のコードでは、runtime·printf
の引数として、ポインタがNULLであるかどうかに応じて異なる値を渡していました。例えば、p ? p->id : -1
という記述は、「p
がNULLでなければ p->id
を、NULLであれば -1
を使用する」という意味です。
ここで問題となるのは、p->id
の型と -1
の型です。p->id
は通常、int32
や int64
のような整数型ですが、-1
はリテラルであり、コンパイラによっては異なる型の扱いを受ける可能性があります。特に、ポインタ型(p
)の評価結果と整数型(-1
)を条件演算子で結合する際に、コンパイラが適切な型昇格を行わない、あるいはARM特有のABIやコンパイラの最適化が絡むことで、予期せぬ型不一致や不正なコード生成が発生したと考えられます。
この問題を解決するため、コミットでは以下のように変更されました。
runtime·schedtrace
関数内に、id1
,id2
,id3
という3つのint64
型のローカル変数を追加しました。- これらの変数に、対応するポインタ(
p
,gp
,lockedg
)がNULLでない場合にのみ、それぞれのID(p->id
,gp->goid
,lockedg->goid
)を代入するように、明示的なif
文を追加しました。ポインタがNULLの場合は、デフォルト値として-1
を代入します。 runtime·printf
の呼び出しにおいて、条件演算子の代わりに、これらの新しく導入されたid1
,id2
,id3
変数を使用するように変更しました。
この変更により、runtime·printf
に渡される引数の型が int64
に統一され、条件演算子による型昇格の曖昧さや、それに起因するコンパイラの問題が解消されました。これは、コードの可読性をわずかに犠牲にしてでも、異なるアーキテクチャでの堅牢なビルドを保証するための実用的なアプローチです。
コアとなるコードの変更箇所
変更は src/pkg/runtime/proc.c
ファイルの runtime·schedtrace
関数内で行われています。
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -2410,6 +2410,7 @@ runtime·schedtrace(bool detailed)
{
static int64 starttime;
int64 now;
+ int64 id1, id2, id3; // <-- 追加された変数
int32 i, q, t, h, s;
int8 *fmt;
M *mp, *lockedm;
@@ -2467,11 +2468,20 @@ runtime·schedtrace(bool detailed)
p = mp->p;
gp = mp->curg;
lockedg = mp->lockedg;
-\t\truntime·printf(" M%d: p=%d curg=%D mallocing=%d throwing=%d gcing=%d"\
+\t\tid1 = -1; // <-- id1の初期化と条件付き代入
+\t\tif(p)
+\t\t\tid1 = p->id;
+\t\tid2 = -1; // <-- id2の初期化と条件付き代入
+\t\tif(gp)
+\t\t\tid2 = gp->goid;
+\t\tid3 = -1; // <-- id3の初期化と条件付き代入
+\t\tif(lockedg)
+\t\t\tid3 = lockedg->goid;
+\t\truntime·printf(" M%d: p=%D curg=%D mallocing=%d throwing=%d gcing=%d"\
" locks=%d dying=%d helpgc=%d spinning=%d lockedg=%D\\n",
-\t\t\tmp->id, p ? p->id : -1, gp ? gp->goid : (int64)-1,\
+\t\t\tmp->id, id1, id2, // <-- id1, id2を使用
\tmp->mallocing, mp->throwing, mp->gcing, mp->locks, mp->dying, mp->helpgc,\
-\t\t\tmp->spinning, lockedg ? lockedg->goid : (int64)-1);
+\t\t\tmp->spinning, id3); // <-- id3を使用
}
for(gp = runtime·allg; gp; gp = gp->alllink) {
\tmp = gp->m;
コアとなるコードの解説
変更前のコードでは、runtime·printf
の引数として、以下のような条件演算子が直接使用されていました。
p ? p->id : -1
gp ? gp->goid : (int64)-1
lockedg ? lockedg->goid : (int64)-1
これらの式は、それぞれ p
, gp
, lockedg
というポインタがNULLでない場合にその構造体のIDメンバー(id
または goid
)を、NULLである場合に -1
を返すことを意図しています。
しかし、C言語の仕様では、条件演算子の第二オペランドと第三オペランドの型が異なる場合、共通の型に昇格されます。例えば、p->id
が int32
で -1
が int
の場合、コンパイラはこれらを共通の型に変換しようとします。この変換プロセスが、特にARMアーキテクチャの特定のコンパイラバージョンにおいて、期待通りに機能しなかったり、コンパイルエラーを引き起こしたりしたと考えられます。例えば、ポインタのサイズと整数のサイズが異なる環境で、ポインタの評価結果と整数リテラルを混在させると、型変換のルールが複雑になり、問題が生じやすくなります。
変更後のコードでは、この問題を回避するために、一時変数 id1
, id2
, id3
を導入し、明示的な if
文を使って値を代入しています。
id1 = -1;
if(p)
id1 = p->id;
id2 = -1;
if(gp)
id2 = gp->goid;
id3 = -1;
if(lockedg)
id3 = lockedg->goid;
この変更により、id1
, id2
, id3
は常に int64
型として定義され、runtime·printf
に渡される前に値が確定します。これにより、条件演算子による型昇格の複雑さがなくなり、コンパイラがより予測可能な方法でコードを生成できるようになります。結果として、ARMアーキテクチャでのビルドが安定し、移植性が向上しました。
この修正は、Goランタイムのような低レベルでパフォーマンスが要求されるコードにおいて、C言語の特定の構文が引き起こす可能性のある微妙な問題を回避するための、堅実なエンジニアリングプラクティスを示しています。
関連リンク
- Go CL (Change List): https://golang.org/cl/12903043
参考にした情報源リンク
- コミットメッセージと差分情報 (
./commit_data/17215.txt
) - C言語の条件演算子に関する一般的な知識
- GoランタイムのM, P, Gモデルに関する一般的な知識
- ARMアーキテクチャとCコンパイラの挙動に関する一般的な知識 (Web検索による補足)