[インデックス 19466] ファイルの概要
このコミットは、Goコンパイラの特定の変更(CL 102820043 / b0ce6dbafc18)を元に戻す(undo)ものです。元の変更は、x=x
のような自己代入におけるクラッシュを修正することを目的としていましたが、32ビットシステム(386およびARMアーキテクチャ)でのビルドを破壊するという深刻な副作用があったため、このコミットで取り消されました。
コミット
commit 9dd062b82e9fc126c787449cc3b4730d0d0525c6
Author: Russ Cox <rsc@golang.org>
Date: Wed May 28 21:46:20 2014 -0400
undo CL 102820043 / b0ce6dbafc18
Breaks 386 and arm builds.
The obvious reason is that this CL only edited 6g/gsubr.c
and failed to edit 5g/gsubr.c and 8g/gsubr.c.
However, the obvious CL applying the same edit to those
files (CL 101900043) causes mysterious build failures
in various of the standard package tests, usually involving
reflect. Something deep and subtle is broken but only on
the 32-bit systems.
Undo this CL for now.
««« original CL description
cmd/gc: fix x=x crash
The 'nodarg' function is used to obtain a Node*
representing a function argument or result.
It returned a brand new Node*, but that violates
the guarantee in most places in the compiler that
two Node*s refer to the same variable if and only if
they are the same Node* pointer. Reestablish that
invariant by making nodarg return a preexisting
named variable if present.
Having fixed that, avoid any copy during x=x in
componentgen, because the VARDEF we emit
before the copy marks the lhs x as dead incorrectly.
The change in walk.c avoids modifying the result
of nodarg. This was the only place in the compiler
that did so.
Fixes #8097.
LGTM=r, khr
R=golang-codereviews, r, khr
CC=golang-codereviews, iant
https://golang.org/cl/102820043
»»»
TBR=r
CC=golang-codereviews, khr
https://golang.org/cl/95660043
---
src/cmd/5g/cgen.c | 8 --------
src/cmd/6g/cgen.c | 7 -------
src/cmd/6g/gsubr.c | 9 ---------
src/cmd/8g/cgen.c | 7 -------
src/cmd/gc/walk.c | 3 +--
test/live.go | 26 --------------------------
6 files changed, 1 insertion(+), 59 deletions(-)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9dd062b82e9fc126c787449cc3b4730d0d0525c6
元コミット内容
このコミットは、以前のコミット b0ce6dbafc18
(CL 102820043) を元に戻すものです。元コミットの目的は、Goコンパイラ(cmd/gc
)における x=x
のような自己代入がクラッシュする問題を修正することでした。
元のコミットで行われた主な変更点は以下の通りです。
-
nodarg
関数の修正:nodarg
関数は、関数の引数または結果を表すNode*
を取得するために使用されます。- 元の実装では、この関数が常に新しい
Node*
を返していました。これは、コンパイラの他のほとんどの場所で保証されている「2つのNode*
が同じ変数を示すのは、それらが同じNode*
ポインタである場合のみ」という不変条件に違反していました。 - 修正では、
nodarg
が既存の名前付き変数を返すように変更され、この不変条件が再確立されました。
-
componentgen
におけるx=x
のコピー回避:nodarg
の修正後、componentgen
関数内でx=x
のような自己代入が行われる際に、不要なコピーを避けるようになりました。- これは、コピーの前に発行される
VARDEF
が、左辺のx
を誤ってデッド(不要)とマークしてしまう問題を回避するためです。
-
walk.c
の変更:walk.c
内のコードが、nodarg
の結果を修正しないように変更されました。これは、コンパイラ内でnodarg
の結果を修正していた唯一の場所でした。
これらの変更は、Goコンパイラの内部的な整合性を保ち、x=x
のような特定のコードパターンでのクラッシュを修正することを目的としていました。この修正は、GoのIssue #8097を解決するものでした。
変更の背景
このコミットの背景には、Goコンパイラの安定性とクロスプラットフォーム互換性の問題があります。元々、cmd/gc: fix x=x crash
というコミット(CL 102820043)は、Goコンパイラが特定の自己代入パターン(例: x = x
)でクラッシュするバグ(Issue #8097)を修正するために導入されました。このバグは、コンパイラの内部表現である Node*
の不変条件が破られていたことに起因していました。
しかし、この修正が導入された後、386およびARMアーキテクチャ(32ビットシステム)でのビルドが壊れるという予期せぬ問題が発生しました。コミットメッセージによると、この問題には2つの側面がありました。
- 不完全な修正: 元のCLは
6g/gsubr.c
のみを編集し、対応する32ビットアーキテクチャのファイルである5g/gsubr.c
(ARM) と8g/gsubr.c
(386) を編集しなかったため、ビルドが失敗しました。 - より深い問題: 上記の明らかな問題を修正しようと、同じ変更を
5g/gsubr.c
と8g/gsubr.c
に適用する別のCL(CL 101900043)が試みられましたが、今度は標準パッケージのテスト(特にreflect
パッケージに関連するもの)で「不可解なビルド失敗」が発生しました。これは、32ビットシステムでのみ発生する、より深く、より微妙な問題が存在することを示唆していました。
これらの問題は、Goコンパイラのクロスコンパイル能力と、異なるアーキテクチャ間でのコード生成の整合性に影響を与えました。特に reflect
パッケージが関与していることから、ランタイムの型情報やメモリレイアウトに関する繊細な問題が示唆されます。
このような状況を受けて、開発チームは、根本的な原因を特定して修正するよりも、まず安定性を回復することを優先しました。そのため、問題を引き起こしている元の変更を一時的に元に戻すという判断が下されました。これが、この「undo」コミットの直接的な背景です。
前提知識の解説
このコミットを理解するためには、Goコンパイラの内部構造、特に以下の概念に関する基本的な知識が必要です。
-
Goコンパイラ (
cmd/gc
):- Go言語の公式コンパイラは、主にGo言語自体で書かれていますが、一部の低レベルな部分はC言語で書かれています。このコミットで言及されている
cgen.c
やgsubr.c
は、C言語で書かれたコンパイラのバックエンド部分に属します。 5g
,6g
,8g
は、それぞれARM (5g), AMD64 (6g), 386 (8g) アーキテクチャ向けのGoコンパイラのバックエンドを指します。Goコンパイラは、異なるアーキテクチャ向けにコードを生成するために、アーキテクチャ固有のコード生成ロジックを持っています。
- Go言語の公式コンパイラは、主にGo言語自体で書かれていますが、一部の低レベルな部分はC言語で書かれています。このコミットで言及されている
-
抽象構文木 (AST) と
Node
:- コンパイラは、ソースコードを解析して抽象構文木(AST)と呼ばれるツリー構造に変換します。ASTの各ノードは、変数、式、ステートメントなどのプログラム要素を表します。
- Goコンパイラでは、これらの要素が
Node
構造体で表現されます。Node*
は、このNode
構造体へのポインタです。 - コンパイラの内部では、同じ論理的な変数やプログラム要素は、常に同じ
Node*
ポインタによって参照されるべきであるという「不変条件」が存在します。これにより、コンパイラは効率的に最適化を行い、変数のライフタイムや使用状況を正確に追跡できます。
-
nodarg
関数:- Goコンパイラのフロントエンド(
cmd/gc
)で使用される関数で、関数の引数や戻り値を表すNode*
を取得する役割を担います。 - この関数が新しい
Node*
を生成してしまうと、上記で述べたNode*
の不変条件が破られ、コンパイラの他の部分で予期せぬ動作やクラッシュを引き起こす可能性があります。
- Goコンパイラのフロントエンド(
-
componentgen
関数:- Goコンパイラのコード生成フェーズで使用される関数の一つで、複合型(構造体、配列など)の要素へのアクセスや代入を処理します。
x = x
のような自己代入の場合、コンパイラは通常、最適化によってこの操作をスキップしようとします。しかし、内部的な状態が正しくない場合、誤ったコードが生成されたり、変数のライフタイム分析が狂ったりする可能性があります。
-
VARDEF
:- コンパイラが変数の定義(Variable Definition)をマークするために使用する内部的な操作または構造です。
VARDEF
が発行されると、コンパイラはその変数が「定義された」と認識し、それ以前の変数の値は「デッド」(不要)とマークされることがあります。x = x
のようなケースでVARDEF
が不適切に発行されると、右辺のx
の値がまだ必要であるにもかかわらず、左辺のx
がデッドとマークされ、誤ったコード生成やクラッシュにつながる可能性があります。
-
walk.c
:- Goコンパイラの
walk
フェーズは、ASTを走査し、最適化やコード生成の準備を行う重要なステップです。 - このフェーズでは、ASTノードが変換されたり、新しいノードが挿入されたりすることがあります。
- Goコンパイラの
-
reflect
パッケージ:- Goの標準ライブラリの一つで、実行時に型情報を検査したり、変数の値を動的に操作したりするための機能を提供します。
reflect
パッケージは、コンパイラが生成する型情報やメモリレイアウトに強く依存しているため、コンパイラの内部的なバグがreflect
を使用するコードに影響を与えることは珍しくありません。特に32ビットシステムでの問題は、ポインタのサイズやアライメント、メモリ管理の微妙な違いに起因することが多いです。
これらの概念を理解することで、このコミットがなぜ行われたのか、そしてそれがGoコンパイラの安定性と正確性にどのように影響するかを深く把握できます。
技術的詳細
このコミットは、Goコンパイラのバックエンドにおける特定の最適化と変数ライフタイム管理の複雑さに起因する問題を浮き彫りにしています。
元のコミット(CL 102820043)は、nodarg
関数が常に新しい Node*
を返すことで、コンパイラ内部の重要な不変条件を破っていた問題を修正しようとしました。この不変条件とは、「同じ論理的な変数やプログラム要素は、常に同じ Node*
ポインタによって参照されるべきである」というものです。この不変条件が破られると、コンパイラは変数の使用状況やライフタイムを正確に追跡できなくなり、結果として x=x
のような自己代入でクラッシュするなどの問題が発生します。
元の修正では、nodarg
が既存の Node*
を返すように変更され、この不変条件が回復されました。これに伴い、componentgen
関数における x=x
の処理も変更されました。具体的には、自己代入の場合に不要なコピーを生成しないようにしました。これは、コピーの前に VARDEF
が発行されると、左辺の x
が誤ってデッドとマークされ、問題を引き起こすためです。
しかし、この修正は32ビットアーキテクチャ(386とARM)で深刻な問題を引き起こしました。
-
アーキテクチャ固有のコード生成の不整合: 元の修正は、主にAMD64 (
6g
) 向けのgsubr.c
に適用されましたが、32ビットアーキテクチャ向けの5g/gsubr.c
と8g/gsubr.c
には適用されませんでした。Goコンパイラは、異なるアーキテクチャ向けに最適化されたコードを生成するために、アーキテクチャ固有のバックエンドコードを持っています。この不整合が、まずビルドエラーの原因となりました。 -
32ビットシステム特有の微妙なバグ: さらに深刻なのは、この修正を32ビットアーキテクチャのファイルにも適用しようとした際に、標準パッケージのテスト(特に
reflect
パッケージ)で「不可解なビルド失敗」が発生したことです。これは、単なるコードの同期不足以上の問題を示唆しています。- ポインタとメモリレイアウト: 32ビットシステムでは、ポインタのサイズが64ビットシステムとは異なります。また、メモリのアライメントやパディングのルールも異なる場合があります。コンパイラが
Node*
の不変条件を扱う際に、これらのアーキテクチャ固有のメモリ特性を考慮しないと、誤ったアドレス計算やデータアクセスが発生し、クラッシュや不正な動作につながります。 reflect
パッケージへの影響:reflect
パッケージは、実行時にGoの型システムと密接に連携し、メモリ上のオブジェクトの構造を検査・操作します。コンパイラが生成する型情報や、変数のメモリレイアウトに関する内部的な不整合は、reflect
パッケージが期待する動作と異なる結果を生み出し、テストの失敗につながる可能性が高いです。例えば、変数のライフタイムが誤って管理されると、reflect
がアクセスしようとしたメモリが既に解放されていたり、不正なデータを含んでいたりする可能性があります。- VARDEFとライブネス分析:
VARDEF
が変数をデッドとマークするタイミングは、コンパイラのライブネス分析(変数が将来使用されるかどうかを判断する分析)に深く関連しています。32ビットシステムでこの分析が何らかの理由で誤動作し、まだライブであるべき変数がデッドとマークされると、その変数へのアクセスが不正となり、reflect
のような動的な操作を行うコードが特に影響を受けやすくなります。
- ポインタとメモリレイアウト: 32ビットシステムでは、ポインタのサイズが64ビットシステムとは異なります。また、メモリのアライメントやパディングのルールも異なる場合があります。コンパイラが
このコミットは、これらの複雑な相互作用を完全に理解し、修正するまでの間、安定性を優先するために元の変更を元に戻すという、実用的な判断を示しています。これは、コンパイラ開発におけるデバッグの難しさと、異なるアーキテクチャ間での微妙な動作の違いを管理することの重要性を強調しています。
コアとなるコードの変更箇所
このコミットは、元の変更を元に戻すため、主に元の変更で追加されたコードを削除し、削除されたコードを元に戻しています。
具体的には、以下のファイルが変更されています。
src/cmd/5g/cgen.c
: 32ビットARMアーキテクチャ向けのコード生成ファイル。元の変更で削除されたcomponentgen
内のx=x
最適化に関するコードが元に戻されています(削除)。src/cmd/6g/cgen.c
: AMD64アーキテクチャ向けのコード生成ファイル。元の変更で削除されたcomponentgen
内のx=x
最適化に関するコードが元に戻されています(削除)。src/cmd/6g/gsubr.c
: AMD64アーキテクチャ向けのサブルーチンファイル。元の変更でnodarg
関数に追加された、既存のNode*
を検索して返すロジックが削除され、元の常に新しいNode*
を返す動作に戻されています。src/cmd/8g/cgen.c
: 32ビット386アーキテクチャ向けのコード生成ファイル。元の変更で削除されたcomponentgen
内のx=x
最適化に関するコードが元に戻されています(削除)。src/cmd/gc/walk.c
: コンパイラのwalk
フェーズのファイル。元の変更でascompatte
関数内のnodarg
の結果を修正しないように変更された部分が元に戻されています。具体的には、a->type = r->type;
がr = nod(OCONVNOP, r, N); r->type = a->type;
に戻されています。test/live.go
: テストファイル。元のコミットで追加された、Issue #8097に関連するf39
,f39a
,f39b
,f39c
というテスト関数が削除されています。これらのテストは、x=x
のような自己代入におけるライブネス分析の問題を検証するためのものでした。
差分から読み取れる具体的な変更点:
-
cgen.c
(5g, 6g, 8g):- // nl and nr are 'cadable' which basically means they are names (variables) now. - // If they are the same variable, don't generate any code, because the - // VARDEF we generate will mark the old value as dead incorrectly. - // (And also the assignments are useless.) - if(nr != N && nl->op == ONAME && nr->op == ONAME && nl == nr) - goto yes;
このコードブロックは、
nl
とnr
が同じ変数である場合にコード生成をスキップするという、元のコミットで追加された最適化ロジックです。このコミットでは、このロジックが削除され、自己代入の場合でもコードが生成される元の動作に戻されています。 -
6g/gsubr.c
:- NodeList *l; ... - if(fp == 1) { - for(l=curfn->dcl; l; l=l->next) { - n = l->n; - if((n->class == PPARAM || n->class == PPARAMOUT) && !isblanksym(t->sym) && n->sym == t->sym) - return n; - } - }
nodarg
関数内で、既存の引数または結果のNode*
を検索して返すためのループが削除されています。これにより、nodarg
は常に新しいNode*
を作成して返す元の動作に戻ります。 -
gc/walk.c
:- r = nod(OCONVNOP, r, N); - r->type = a->type; + a->type = r->type;
ascompatte
関数内で、nodarg
の結果 (a
) の型をr
の型に直接設定するのではなく、r
をOCONVNOP
ノードでラップして型を設定する元のロジックに戻されています。これは、nodarg
が新しいNode*
を返すようになったため、その型を適切に設定し直す必要があるためです。 -
test/live.go
: Issue #8097に関連するテストケースが完全に削除されています。これは、元の修正が元に戻されたため、その修正によって解決されるはずだった問題のテストも不要になったためです。
これらの変更は、元のコミットが導入したロジックを体系的に削除し、Goコンパイラのコードベースを以前の安定した状態に戻すことを目的としています。
コアとなるコードの解説
このコミットのコアとなる変更は、Goコンパイラの内部における Node*
の管理と、それに関連する変数ライフタイム分析のロジックを元に戻すことです。
-
nodarg
関数の役割と変更の意図:nodarg
は、Go関数の引数や戻り値をコンパイラの内部表現であるNode*
として取得する関数です。- 元のコミット(CL 102820043)では、この関数が常に新しい
Node*
を生成して返していたことが問題視されました。コンパイラの設計原則として、「同じ論理的な変数やプログラム要素は、常に同じNode*
ポインタによって参照されるべき」という不変条件があります。これにより、コンパイラは変数の参照を効率的に追跡し、最適化やライブネス分析を正確に行うことができます。 - 新しい
Node*
が生成されると、この不変条件が破られ、例えばx = x
のような自己代入の場合に、左辺のx
と右辺のx
が異なるNode*
ポインタで表現されてしまい、コンパイラがこれらを別々の変数として扱ってしまう可能性がありました。これが、Issue #8097で報告されたクラッシュの原因でした。 - 元の修正では、
nodarg
が既存の変数に対応するNode*
を検索し、もし見つかればそれを返すように変更されました。これにより、不変条件が回復され、x = x
のようなケースで同じNode*
が使用されることが保証されるはずでした。 - この「undo」コミットでは、この修正が元に戻され、
nodarg
は再び常に新しいNode*
を返すようになります。 これは、32ビットシステムでのビルド問題が解決できなかったため、一時的に元の(問題のある)動作に戻すことを意味します。
-
componentgen
におけるx=x
最適化の取り消し:componentgen
は、複合型の要素へのアクセスや代入を処理するコード生成関数です。- 元のコミットでは、
x = x
のような自己代入の場合に、不要なコード生成を避けるための最適化が導入されました。具体的には、左辺と右辺が同じ変数(同じNode*
)であると判断された場合、代入操作自体をスキップするというロジックです。 - この最適化は、
VARDEF
(変数の定義をマークするコンパイラの内部操作)が、コピーの前に左辺のx
を誤ってデッドとマークしてしまう問題を回避するためでもありました。もしVARDEF
が不適切に発行されると、右辺のx
の値がまだ必要であるにもかかわらず、左辺のx
がデッドと見なされ、不正なメモリアクセスやクラッシュにつながる可能性があります。 - この「undo」コミットでは、この
x=x
最適化ロジックが削除されます。 これにより、自己代入の場合でも、コンパイラは通常通り代入コードを生成するようになります。これは、最適化による副作用が32ビットシステムで問題を引き起こしたため、安全策として元に戻されたものです。
-
walk.c
における型の再設定ロジックの復元:walk.c
は、ASTを走査し、コード生成の準備を行うフェーズです。- 元のコミットでは、
ascompatte
関数内でnodarg
の結果を直接変更しないように修正されていました。これは、nodarg
が既存のNode*
を返すようになったため、そのNode*
の型を直接変更すると、他の場所での参照に影響を与える可能性があったためです。 - この「undo」コミットでは、この変更が元に戻され、
nodarg
の結果 (a
) の型をr
の型に直接設定するのではなく、r
をOCONVNOP
ノードでラップして型を設定する元のロジックが復元されます。 これは、nodarg
が再び新しいNode*
を返すようになったため、その新しいNode*
の型を適切に設定し直す必要があるためです。
これらの変更は、Goコンパイラの内部的な整合性、特に Node*
ポインタの一意性と変数ライフタイム分析の正確性に関するものです。この「undo」コミットは、元の修正が32ビットシステムで引き起こした予期せぬ複雑な問題に対処するための一時的な措置であり、根本的な解決策が見つかるまでの安定化を目的としています。
関連リンク
- 元のコミット (CL 102820043): https://golang.org/cl/102820043
- 関連するGo Issue #8097: https://github.com/golang/go/issues/8097
- このundoコミットのCL (CL 95660043): https://golang.org/cl/95660043
参考にした情報源リンク
- Go言語のコンパイラに関する一般的な情報源(Goの公式ドキュメント、Goコンパイラのソースコード自体)
- Goコンパイラの内部構造に関するブログ記事や解説(例: "Go's Compiler: An Overview" by Russ Cox, "The Go Programming Language Compiler" by Rob Pike, Ken Thompson, Russ Cox, and Ian Lance Taylor)
- GoのIssueトラッカーでの関連する議論
- Goのコードレビューシステム (Gerrit) での関連するCLの議論
- Goの
reflect
パッケージに関するドキュメントと解説