[インデックス 17491] ファイルの概要
このコミットは、Goコンパイラ(cmd/5g
, cmd/6g
, cmd/8g
)におけるビットマップ生成ロジック内のfor
ループを簡素化するものです。具体的には、ループの初期化部分からインデックス変数j
の増分処理を分離し、ループ本体の最後に移動させることで、Plan 9環境でのコンパイラの誤コンパイル(miscompilation)問題を解決しています。
コミット
cmd/5g, cmd/6g, cmd/8g: simplify for loop in bitmap generation
Lucio De Re reports that the more complex
loop miscompiles on Plan 9.
R=ken2
CC=golang-dev
https://golang.org/cl/13602043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/af2a3193afde74fc6b653f31fb3c65f836f231d8
元コミット内容
cmd/5g, cmd/6g, cmd/8g: simplify for loop in bitmap generation
Lucio De Re reports that the more complex
loop miscompiles on Plan 9.
R=ken2
CC=golang-dev
https://golang.org/cl/13602043
変更の背景
この変更は、Lucio De Re氏によって報告された、Plan 9オペレーティングシステム上でGoコンパイラが特定の複雑なfor
ループを誤ってコンパイルするという問題に対処するために行われました。誤コンパイルは、生成されるバイナリが期待通りに動作しない原因となり、Goプログラムの信頼性と移植性に影響を与えます。
Goコンパイラは、ガベージコレクション(GC)のためにスタック上のポインタの位置を示す「スタックマップ」または「ビットマップ」を生成します。このビットマップ生成ロジック内で使用されていたfor
ループが、Plan 9のCコンパイラ(Goコンパイラ自体はC言語で書かれていた時期があるため、Plan 9上でGoコンパイラをビルドする際にPlan 9のCコンパイラが使用される)によって正しく処理されなかったと考えられます。
コンパイラの誤コンパイルは、コンパイラ自身のバグや、特定の最適化パスが予期せぬ副作用を引き起こすことによって発生することがあります。このケースでは、ループの初期化とインデックス変数の増分が同じ行で行われている「より複雑な」形式が、Plan 9のCコンパイラにとって問題を引き起こしたようです。ループの構造を簡素化することで、この特定のコンパイラの誤動作を回避し、Goコンパイラの堅牢性を向上させることを目的としています。
前提知識の解説
Goコンパイラ (cmd/5g
, cmd/6g
, cmd/8g
)
Go言語の初期のコンパイラは、ターゲットアーキテクチャごとに異なる名前を持っていました。
cmd/5g
: ARMアーキテクチャ (ARMv5, ARMv6, ARMv7) 向けのGoコンパイラ。cmd/6g
: AMD64 (x86-64) アーキテクチャ向けのGoコンパイラ。cmd/8g
: x86 (32-bit) アーキテクチャ向けのGoコンパイラ。
これらのコンパイラは、Goのソースコードを各アーキテクチャの機械語に変換する役割を担っていました。現在では、go tool compile
という統一されたコマンドの下で、これらの機能が提供されていますが、内部的には同様のアーキテクチャ固有のコード生成ロジックが引き継がれています。これらのコンパイラは、Goのランタイムと密接に連携し、特にガベージコレクションのためのメタデータ生成において重要な役割を果たします。
ビットマップ生成とスタックマップ
Go言語は、正確なガベージコレクション(Precise GC)を採用しています。これは、ガベージコレクタがメモリ上のどの値がポインタであり、どの値がポインタでないかを正確に識別できることを意味します。これにより、誤って非ポインタをポインタとして扱い、誤ったメモリを解放したり、到達可能なオブジェクトを回収したりするのを防ぎます。
この正確なGCを実現するために、Goコンパイラは各関数のスタックフレームに関するメタデータ、特に「スタックマップ」または「ビットマップ」を生成します。スタックマップは、特定の時点(例えば、関数呼び出し時やGCのトリガー時)において、スタック上のどの位置にポインタが格納されているかを示すビット列です。
bvget(bv, j)
: この関数は、ビットベクトルbv
のj
番目のビットを取得します。ビットベクトルは、スタック上のポインタの位置を示すために使用されます。stkptrsize
: スタックフレーム内のポインタの合計サイズ。stkzerosize
: スタックフレーム内でゼロ初期化されるべき領域のサイズ。widthptr
: ポインタの幅(通常は4バイトまたは8バイト)。
これらの情報を用いて、defframe
関数(スタックフレームの定義に関連する関数)内で、スタック上のポインタの位置を特定し、それに基づいて必要なコード(例えば、ポインタをゼロ初期化するコード)を生成します。
Plan 9
Plan 9 from Bell Labsは、Unixの設計思想をさらに推し進めた分散オペレーティングシステムです。その設計は、すべてをファイルとして扱うという原則に基づいています。Plan 9には独自のCコンパイラツールチェインがあり、Go言語の初期開発においては、Plan 9のツールがGoコンパイラ自体のビルドにも使用されていました。
Plan 9のCコンパイラは、特定の最適化やコード生成の挙動が、他の一般的なCコンパイラ(GCCやClangなど)とは異なる場合があります。このコミットで報告された「誤コンパイル」は、Plan 9のCコンパイラが、Goコンパイラのソースコード内の特定のループ構造を正しく解釈または最適化できなかったことに起因すると考えられます。
技術的詳細
このコミットの技術的詳細なポイントは、コンパイラの最適化と、特定のコンパイラ(この場合はPlan 9のCコンパイラ)の挙動の差異にあります。
元のコードでは、for
ループの初期化部分でi
とj
の両方の変数を初期化し、ループの増分部分で両方を更新していました。
// 変更前 (例: src/cmd/5g/ggen.c)
for(i=0, j=(stkptrsize-stkzerosize)/widthptr*2; i<stkzerosize; i+=widthptr, j+=2) {
// ...
}
この形式はC言語の標準的な構文であり、多くのコンパイラで問題なく処理されます。しかし、Plan 9のCコンパイラは、この「より複雑な」ループ構造、特に複数の変数を初期化・更新する形式に対して、内部的な最適化パスやコード生成ロジックで予期せぬバグや非互換性を持っていた可能性があります。
変更後のコードでは、変数j
の初期化をループの直前に分離し、j
の増分処理をループ本体の最後に移動させています。
// 変更後 (例: src/cmd/5g/ggen.c)
j = (stkptrsize - stkzerosize)/widthptr * 2; // jの初期化を分離
for(i=0; i<stkzerosize; i+=widthptr) { // ループの初期化と増分を簡素化
// ...
j += 2; // jの増分をループ本体の最後に移動
}
この変更により、for
ループのヘッダ部分がfor(i=0; i<stkzerosize; i+=widthptr)
という、より単純な形式になります。これにより、Plan 9のCコンパイラがこのループをより予測可能な方法で処理できるようになり、誤コンパイルが回避されたと考えられます。
この問題は、GoコンパイラがC言語で書かれており、異なるプラットフォーム(この場合はPlan 9)でビルドされる際に、そのプラットフォームのCコンパイラの特性に依存するという、当時のGo開発におけるクロスコンパイルの複雑さを示しています。コンパイラのバグは非常にデバッグが困難であり、このような簡素化による回避策は、特定の環境での安定性を確保するための実用的なアプローチです。
コアとなるコードの変更箇所
変更は、src/cmd/5g/ggen.c
、src/cmd/6g/ggen.c
、src/cmd/8g/ggen.c
の3つのファイルにわたって行われています。これらはそれぞれARM、AMD64、x86アーキテクチャ向けのGoコンパイラのジェネレータ部分です。
各ファイルで、defframe
関数内のビットマップ生成に関連するfor
ループが変更されています。
src/cmd/5g/ggen.c
--- a/src/cmd/5g/ggen.c
+++ b/src/cmd/5g/ggen.c
@@ -49,7 +49,8 @@ defframe(Prog *ptxt, Bvec *bv)\n \t\tpatch(p, p1);\n \t} else {\n \t\tfirst = 1;\n-\t\tfor(i=0, j=(stkptrsize-stkzerosize)/widthptr*2; i<stkzerosize; i+=widthptr, j+=2) {\n+\t\tj = (stkptrsize - stkzerosize)/widthptr * 2;\n+\t\tfor(i=0; i<stkzerosize; i+=widthptr) {\n \t\t\tif(bvget(bv, j) || bvget(bv, j+1)) {\n \t\t\t\tif(first) {\n \t\t\t\t\tp = appendp(p, AMOVW, D_CONST, NREG, 0, D_REG, 0, 0);\n@@ -57,6 +58,7 @@ defframe(Prog *ptxt, Bvec *bv)\n \t\t\t\t}\n \t\t\t\tp = appendp(p, AMOVW, D_REG, 0, 0, D_OREG, REGSP, 4+frame-stkzerosize+i);\n \t\t\t}\n+\t\t\tj += 2;\n \t\t}\n \t}\n }\n```
### `src/cmd/6g/ggen.c`
```diff
--- a/src/cmd/6g/ggen.c
+++ b/src/cmd/6g/ggen.c
@@ -37,9 +37,12 @@ defframe(Prog *ptxt, Bvec *bv)\n \t\tp = appendp(p, AREP, D_NONE, 0, D_NONE, 0);\n \t\tappendp(p, ASTOSQ, D_NONE, 0, D_NONE, 0);\n \t} else {\n-\t\tfor(i=0, j=(stkptrsize-stkzerosize)/widthptr*2; i<stkzerosize; i+=widthptr, j+=2)\n+\t\tj = (stkptrsize - stkzerosize)/widthptr * 2;\n+\t\tfor(i=0; i<stkzerosize; i+=widthptr) {\n \t\t\tif(bvget(bv, j) || bvget(bv, j+1))\n \t\t\t\tp = appendp(p, AMOVQ, D_CONST, 0, D_SP+D_INDIR, frame-stkzerosize+i);\n+\t\t\tj += 2;\n+\t\t}\n \t}\n }\n \n```
### `src/cmd/8g/ggen.c`
```diff
--- a/src/cmd/8g/ggen.c
+++ b/src/cmd/8g/ggen.c
@@ -39,9 +39,12 @@ defframe(Prog *ptxt, Bvec *bv)\n \t\tp = appendp(p, AREP, D_NONE, 0, D_NONE, 0);\n \t\tappendp(p, ASTOSL, D_NONE, 0, D_NONE, 0);\n \t} else {\n-\t\tfor(i=0, j=(stkptrsize-stkzerosize)/widthptr*2; i<stkzerosize; i+=widthptr, j+=2)\n+\t\tj = (stkptrsize - stkzerosize)/widthptr * 2;\n+\t\tfor(i=0; i<stkzerosize; i+=widthptr) {\n \t\t\tif(bvget(bv, j) || bvget(bv, j+1))\n \t\t\t\tp = appendp(p, AMOVL, D_CONST, 0, D_SP+D_INDIR, frame-stkzerosize+i);\n+\t\t\tj += 2;\n+\t\t}\n \t}\n }\n \n```
## コアとなるコードの解説
これらの変更は、`defframe`関数内の`for`ループの構造を簡素化することを目的としています。`defframe`関数は、Goのランタイムがガベージコレクションのためにスタックフレームをどのように扱うかを定義する際に使用される、重要なコンパイラ内部関数です。特に、スタック上のポインタを追跡するためのビットマップを生成するロジックが含まれています。
変更前は、`for`ループのヘッダで2つの変数`i`と`j`を同時に初期化し、同時に増分していました。
`for(i=0, j=(stkptrsize-stkzerosize)/widthptr*2; i<stkzerosize; i+=widthptr, j+=2)`
この行は、`i`がスタックのオフセットを、`j`がビットマップ内の対応するビットのインデックスを追跡していることを示唆しています。
変更後では、このループが以下のように分割されました。
1. `j`の初期化をループの直前に移動: `j = (stkptrsize - stkzerosize)/widthptr * 2;`
2. `for`ループのヘッダから`j`の増分を削除し、`i`のみを扱うように簡素化: `for(i=0; i<stkzerosize; i+=widthptr)`
3. `j`の増分をループ本体の最後に移動: `j += 2;`
この変更の意図は、Plan 9のCコンパイラが、複数の変数を同時に初期化・増分するような複雑な`for`ループの構文を正しく処理できない、または最適化の過程でバグを引き起こす可能性があるため、それを回避することにあります。ループの構造をより単純な形式にすることで、コンパイラが意図した通りの機械語を生成しやすくなり、誤コンパイルの問題が解決されます。
機能的には、この変更はビットマップ生成ロジックの動作を変えるものではありません。`i`と`j`は依然として同期して増分され、スタック上のポインタ位置とビットマップの対応関係は維持されます。しかし、コードの表現方法が変更されたことで、特定のコンパイラ環境での堅牢性が向上しました。
## 関連リンク
* GitHubコミット: [https://github.com/golang/go/commit/af2a3193afde74fc6b653f31fb3c65f836f231d8](https://github.com/golang/go/commit/af2a3193afde74fc6b653f31fb3c65f836f231d8)
* Go CL (Change List): [https://golang.org/cl/13602043](https://golang.org/cl/13602043)
## 参考にした情報源リンク
* Go言語のガベージコレクションとスタックマップに関する情報:
* [https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFIwimQEx_iqeuuLuNYTB3BHW9X6RpNLbGF1zyw_L-4ShvjF7h8UJ3UBPah2kFSMKJBtMD0Fi4-wvohfW8lNN--YFaSN5tpuA17pbtVvfbyPQJnvAVtWrqBnIEermT00HG_iACRC5-5MOaJc30_vDhqiuc4tK3RHqAG6XwilbyOyNRIGBBsL8TBkoJsj7TIFgAb_9Fc-tL5rInyfay39iB5hO-C6MIPKSaRRqTTc6R988r_YV5VZP3Zngbfuh5tFWAvLshAOBw=](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFIwimQEx_iqeuuLuNYTB3BHW9X6RpNLbGF1zyw_L-4ShvjF7h8UJ3UBPah2kFSMKJBtMD0Fi4-wvohfW8lNN--YFaSN5tpuA17pbtVvfbyPQJnvAVtWrqBnIEermT00HG_iACRC5-5MOaJc30_vDhqiuc4tK3RHqAG6XwilbyOyNRIGBBsL8TBkoJsj7TIFgAb_9Fc-tL5rInyfay39iB5hO-C6MIPKSaRRqTTc6R988r_YV5VZP3Zngbfuh5tFWAvLshAOBw=)
* [https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFkntuGt2kHGOiFPblB15knBB7mVeiG-Qmpz0w9E6RECcW02vcNH_-seM8By_F0FFiHO2kKhZoSD_ayX8bOtrS4qcp3qh4y-WGJ1MJFpbWZwQcCl9zIg9-T30JqUWsA](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFkntuGt2kHGOiFPblB15knBB7mVeiG-Qmpz0w9E6RECcW02vcNH_-seM8By_F0FFiHO2kKhZoSD_ayX8bXo_trS4qcp3qh4y-WGJ1MJFpbWZwQcCl9zIg9-T30JqUWsA)
* [https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGi0czYEMn9vCQh4xQSYwkSq3c8JMOuoql4mQyucJ2BFnQR91bVSbW8gyD2rtE_-J1XLpP_Uk2VLNYMx2UzHNJr1iZdFQYcCbhpgevav2Q9BT73fw2v9XL-TpMRzHVD2ddGvtcsZgej0xSSKCNzsdI=](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGi0czYEMn9vCQh4xQSYwkSq3c8JMOuoql4mQyucJ2BFnQR91bVSbW8gyD2rtE_-J1XLpP_Uk2VLNYMx2UzHNJr1iZdFQYcCbhpgevav2Q9BT73fw2v9XL-TpMRzHVD2ddGvtcsZgej0xSSKCNzsdI=)
* Plan 9 Cコンパイラに関する情報:
* [https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFWiykumOdiCti2-VT9InQXZ2-IUeUgD5nuQMcKcjcvi_ufhXypqs07OwsPlGIBCbY14skmdadYyaPN3ABOEk7ncwQqR76Jlls5ykyPidCEBM1jAAAkoMjr1I9Lg4s=](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFWiykumOdiCti2-VT9InQXZ2-IUeUgD5nuQMcKcjcvi_ufhXypqs07OwsPlGIBCbY14skmdadYyaPN3ABOEk7ncwQqR76Jlls5ykyPidCEBM1jAAAkoMjr1I9Lg4s=)
* [https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQE1xVMf4QpbGuDi3oWFdrUsekIGb-m0GZQZARTifXRAz-_9tai7HhnWllj-wDvbRRUSiDLK_Q7LlEP80Hjijg9Qif-jcJrI0CJUC1SpptjTbVHqYLemnIs](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQE1xVMf4QpbGuDi3oWFdrUsekIGb-m0GZQZARTifXRAz-_9tai7HhnWllj-wDvbRRUSiDLK_Q7LlEP80Hjijg9Qif-jcJrI0CJUC1SpptjTbVHqYLemnIs)
* [https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQG6v2SEUB7MrEQvwah0BEoK224l5kxne7JozXy6NQIfQ9ZTDDfpSkwcyiMy5ZLWr2Yo5Ulyu_3c7lWz5TXWYtA3vRE6szFEDcnM-jDKlMxyK6iKcQfZyyU3VxmMOD-IXgMc9Q==](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQG6v2SEUB7MrEQvwah0BEoK224l5kxne7JozXy6NQIfQ9ZTDDfpSkwcyiMy5ZLWr2Yo5Ulyu_3c7lWz5TXWYtA3vRE6szFEDcnM-jDKlMxyK6iKcQfZyyU3VxmMOD-IXgMc9Q==)