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

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

このコミットは、Goコンパイラの一部であるcmd/5g(ARMアーキテクチャ向けコンパイラ)におけるレジスタ最適化のバグ修正に関するものです。具体的には、変数のアドレスが取得されたことをコンパイラが正しく認識できず、その結果、変数が設定されたにもかかわらず使用されていないと誤って判断し、その設定(代入)を最適化によって削除してしまう問題に対処しています。

コミット

commit 658482d70f10962e44801565f059e26d85bf4746
Author: Russ Cox <rsc@golang.org>
Date:   Sat Sep 22 10:01:35 2012 -0400

    cmd/5g: fix register opt bug
    
    The width was not being set on the address, which meant
    that the optimizer could not find variables that overlapped
    with it and mark them as having had their address taken.
    This let to the compiler believing variables had been set
    but never used and then optimizing away the set.
    
    Fixes #4129.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/6552059

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

https://github.com/golang/go/commit/658482d70f10962e44801565f059e26d85bf4746

元コミット内容

cmd/5g: fix register opt bug
    
The width was not being set on the address, which meant
that the optimizer could not find variables that overlapped
with it and mark them as having had their address taken.
This let to the compiler believing variables had been set
but never used and then optimizing away the set.

Fixes #4129.

R=ken2
CC=golang-dev
https://golang.org/cl/6552059

変更の背景

この変更は、Goコンパイラ(特にARMアーキテクチャ向けの5g)におけるレジスタ最適化の誤動作を修正するために行われました。問題の根源は、変数のアドレスが取得された際に、そのアドレスの「幅」(データサイズ)が適切に設定されていなかったことにあります。

コンパイラの最適化フェーズでは、変数がメモリ上でどのように配置され、どの範囲を占めるかを正確に把握する必要があります。特に、ある変数のアドレスが別の変数と「オーバーラップ」している場合(例えば、構造体内のフィールドや配列の要素など)、コンパイラはそのオーバーラップを認識し、アドレスが取得された変数を「アドレスが取られた」ものとしてマークする必要があります。これにより、その変数が間接的にアクセスされる可能性があることをコンパイラに伝え、不適切な最適化(例えば、変数の代入が未使用と判断されて削除されるなど)を防ぎます。

しかし、このバグにより、アドレスの幅が正しく設定されないため、コンパイラは変数のオーバーラップを検出できず、結果として、変数が設定されたにもかかわらず、その値が後で使われないと誤解釈していました。このような誤解釈は、コンパイラが「デッドコード削除」のような最適化を行う際に、実際に必要な代入処理を削除してしまうという深刻な問題を引き起こします。これは、プログラムの動作が意図しないものになる、あるいはクラッシュするといった形で現れる可能性があります。

この問題は、GoのIssueトラッカーで「#4129」として報告されており、このコミットはその報告されたバグを修正することを目的としています。

前提知識の解説

このコミットを理解するためには、以下の概念について基本的な知識があると役立ちます。

  • コンパイラ最適化: コンパイラが生成する機械語コードの性能(速度、サイズなど)を向上させるためのプロセス。これには、デッドコード削除、レジスタ割り当て、ループ最適化など、様々な手法が含まれます。
  • レジスタ割り当て: プログラムの実行中に頻繁にアクセスされる変数を、CPUの高速なレジスタに割り当てる最適化手法。これにより、メモリへのアクセス回数を減らし、実行速度を向上させます。
  • アドレスが取られた変数 (Address-taken variables): プログラム内でそのメモリ上のアドレスが明示的に取得された変数。例えば、C言語の&演算子やGo言語のポインタ変数への代入などによってアドレスが取得されます。アドレスが取られた変数は、ポインタを介して間接的にアクセスされる可能性があるため、コンパイラは通常、その変数をレジスタに割り当てることを避け、メモリ上に保持します。また、その変数の値が変更される可能性があるため、デッドコード削除などの最適化の対象から外れることがあります。
  • データ幅 (Width): 変数やデータ型がメモリ上で占めるバイト数。例えば、32ビット整数は4バイトの幅を持ちます。コンパイラは、メモリレイアウトやアドレス計算を行う際にこの幅を正確に知る必要があります。
  • cmd/5g, cmd/6g, cmd/8g: これらは、Go言語の初期のコンパイラツールチェーンの一部です。
    • 5g: ARMアーキテクチャ(例: ARMv5, ARMv6, ARMv7)向けのGoコンパイラ。
    • 6g: AMD64(x86-64)アーキテクチャ向けのGoコンパイラ。
    • 8g: x86(32ビット)アーキテクチャ向けのGoコンパイラ。 これらのコンパイラは、Goのソースコードを各アーキテクチャの機械語に変換する役割を担っていました。現在のGoでは、これらのコンパイラは統合され、go tool compileコマンドを通じて利用されますが、内部的には同様の最適化パスが存在します。
  • NodeAddr: コンパイラの内部表現におけるデータ構造です。
    • Node: 抽象構文木(AST)のノードを表す構造体で、変数、式、ステートメントなどを表現します。
    • Addr: メモリアドレスやレジスタなど、データの場所を表す構造体です。
  • etype (Element Type): Goコンパイラ内部で使われる型情報の一つで、変数の基本型(整数、浮動小数点数、ポインタなど)を示します。
  • width: 変数や型が占めるメモリ上のサイズ(バイト単位)。
  • offset: 構造体や配列内で、ある要素が先頭からどれだけオフセットしているかを示す値。

技術的詳細

このコミットの技術的な核心は、コンパイラのnaddr関数とregopt関数におけるwidth(データ幅)の取り扱いを修正することにあります。

naddr関数の修正 (src/cmd/5g/gsubr.c)

naddr関数は、抽象構文木(AST)のNodeから、そのノードが参照するメモリ上のアドレス(Addr構造体)を構築する役割を担っています。この関数は、変数の型情報に基づいて、そのアドレスのwidthフィールドを適切に設定する必要があります。

修正前は、naddr関数がAddr構造体のwidthフィールドを常に正しく設定していませんでした。特に、NodeT(型)またはTIDEAL(理想型、例えば型推論前の数値リテラルなど)でない場合に、n->type->widtha->widthに代入する処理が欠落していました。

このコミットでは、以下のコードが追加されました。

	if(n->type != T && n->type->etype != TIDEAL) {
		dowidth(n->type);
		a->width = n->type->width;
	}

このコードは、nが具体的な型を持つ変数である場合に、その型の幅を計算し(dowidth(n->type))、その結果をAddr構造体awidthフィールドに設定します。これにより、Addrが常に正しいデータ幅を持つようになります。

さらに、naddr関数の最後に以下のチェックが追加されました。

	if(a->width < 0)
		fatal("naddr: bad width for %N -> %D", n, a);

これは、naddrが不正な負の幅を設定してしまった場合に、コンパイラが致命的なエラーを発生させることで、早期に問題を検出するための防御的なチェックです。

regopt関数の修正 (src/cmd/5g/reg.c, src/cmd/6g/reg.c, src/cmd/8g/reg.c)

regopt関数は、レジスタ割り当て最適化の主要なパスです。この関数は、プログラム内の変数を分析し、どの変数をレジスタに割り当てるか、どの変数がメモリ上に残るべきかを決定します。このプロセスの一部として、変数のアドレスが取られているかどうかを追跡し、それに応じて最適化の挙動を調整します。

修正前は、regopt関数内で変数のデバッグ情報を出力する際に、print文がコメントアウトされていました。このコミットでは、デバッグフラグdebug['R']debug['v']が設定されている場合に、このデバッグ情報を出力するように変更されました。

		if(debug['R'] && debug['v'])
			print("bit=%2d addr=%d et=%-6E w=%-2d s=%N + %lld\n",
				i, v->addr, v->etype, v->width, v->node, v->offset);

この変更自体はバグ修正に直接関係ありませんが、最適化パスのデバッグと理解を深めるために重要です。

より重要な修正は、mkvar関数(regoptから呼び出される)におけるwidthの検証です。mkvar関数は、レジスタ割り当てのために変数を内部表現に変換する際に、その変数の幅を考慮します。

以下のチェックがmkvar関数に追加されました。

	if(w < 0)
		fatal("bad width %d for %D", w, a);

これは、naddr関数と同様に、変数の幅が不正な負の値である場合に、コンパイラが致命的なエラーを発生させるための防御的なチェックです。naddrで幅が正しく設定されるようになったため、このfatalは通常は発生しないはずですが、コンパイラの堅牢性を高めるために追加されました。

全体的な影響

これらの変更により、コンパイラは変数のアドレスが取られた際に、その変数のメモリ上の幅を正確に認識できるようになります。これにより、最適化器が変数のオーバーラップを正しく検出し、「アドレスが取られた」変数としてマークできるようになります。結果として、コンパイラが変数の代入を誤ってデッドコードとして削除するバグが修正され、生成されるコードの正確性と信頼性が向上します。

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

このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。

  1. src/cmd/5g/gsubr.c:

    • naddr関数内で、Nodeの型がTまたはTIDEALでない場合に、Addr構造体のwidthフィールドをn->type->widthで設定するロジックが追加されました。
    • naddr関数の最後に、a->widthが負の値である場合にfatalエラーを発生させるチェックが追加されました。
  2. src/cmd/5g/reg.c:

    • regopt関数内のデバッグ出力(print文)がコメントアウトから有効化されました(debug['R'] && debug['v']の場合)。
    • mkvar関数内で、変数の幅wが負の値である場合にfatalエラーを発生させるチェックが追加されました。
    • regopt関数の各最適化パス(pass1, pass2, pass2.5, pass3, pass4, pass4.5, pass5, pass6, pass7)の後に、デバッグフラグが有効な場合にdumpit関数を呼び出す行が追加されました。これはデバッグ目的の変更です。
  3. src/cmd/6g/reg.c:

    • regopt関数内のデバッグ出力(print文)がコメントアウトから有効化されました。
    • mkvar関数内で、変数の幅wが負の値である場合にfatalエラーを発生させるチェックが追加されました。
    • mkvar関数内の別のデバッグ出力のprint文が修正されました。
  4. src/cmd/8g/reg.c:

    • regopt関数内のデバッグ出力(print文)がコメントアウトから有効化されました。
    • mkvar関数内で、変数の幅wが負の値である場合にfatalエラーを発生させるチェックが追加されました。

コアとなるコードの解説

src/cmd/5g/gsubr.c の変更

--- a/src/cmd/5g/gsubr.c
+++ b/src/cmd/5g/gsubr.c
@@ -1199,6 +1199,11 @@ naddr(Node *n, Addr *a, int canemitcode)\n 	if(n == N)\n 		return;\n \n+	if(n->type != T && n->type->etype != TIDEAL) {\n+		dowidth(n->type);\n+		a->width = n->type->width;\n+	}\n+\n 	switch(n->op) {\n 	default:\n 		fatal("naddr: bad %O %D", n->op, a);\
@@ -1378,6 +1383,9 @@ naddr(Node *n, Addr *a, int canemitcode)\n 			fatal("naddr: OADDR %d\n", a->type);\n 		}\n 	}\n+\t\n+\tif(a->width < 0)\n+\t\tfatal("naddr: bad width for %N -> %D", n, a);\
 }\n \n /*
  • if(n->type != T && n->type->etype != TIDEAL): この条件は、nが具体的なデータ型を持つノードであるかどうかをチェックしています。Tは一般的な型を表す内部定数、TIDEALは型がまだ確定していない理想型(例: 10のような数値リテラル)を表します。つまり、この条件は「型が確定している変数ノードの場合」を意味します。
  • dowidth(n->type);: dowidth関数は、与えられた型(n->type)のメモリ上の幅を計算し、その型情報に設定します。これは、構造体や配列のような複合型の場合に、その要素の幅を再帰的に計算するなどして、正確な合計幅を決定するために必要です。
  • a->width = n->type->width;: dowidthによって計算されたn->typeの幅を、Addr構造体awidthフィールドに代入しています。これにより、naddr関数が構築するアドレス情報が、参照する変数の正しいメモリ幅を持つようになります。これが、最適化器がオーバーラップを正しく検出するために不可欠な情報となります。
  • if(a->width < 0) fatal(...): naddr関数がアドレスを構築した後、そのwidthが負の値になっていないかをチェックします。負の幅は不正な状態を示し、コンパイラの内部エラーを意味するため、ここで早期に検出して終了させます。

src/cmd/5g/reg.c の変更(mkvar関数部分)

--- a/src/cmd/5g/reg.c
+++ b/src/cmd/5g/reg.c
@@ -935,6 +964,8 @@ mkvar(Reg *r, Adr *a)\n 	et = a->etype;\n 	o = a->offset;\n 	w = a->width;\
+	if(w < 0)\n+		fatal("bad width %d for %D", w, a);\
 \n 	for(i=0; i<nvar; i++) {\
 	\tv = var+i;\
  • w = a->width;: mkvar関数は、引数として渡されたAddr構造体aから、その幅wを取得します。
  • if(w < 0) fatal(...): ここでも、取得した幅wが負の値である場合にfatalエラーを発生させています。これは、naddr関数で設定された幅が、何らかの理由で不正な値になっていた場合に備えるための防御的なチェックです。naddrの修正により、このfatalがトリガーされることはなくなるはずですが、コンパイラの堅牢性を高めるために残されています。

これらの変更は、コンパイラの内部で変数のメモリレイアウトに関する情報(特に幅)が正確に伝達されるようにすることで、レジスタ最適化器が変数の「アドレスが取られた」状態を正しく認識し、不適切な最適化(デッドコード削除)を防ぐという、根本的なバグ修正に貢献しています。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (GitHub): https://github.com/golang/go
  • コンパイラ最適化に関する一般的な情報源 (例: Wikipedia, コンパイラ設計の教科書)
  • Go言語の初期のコンパイラに関するドキュメントやブログ記事 (もしあれば)

(注: Issue #4129の具体的な内容については、Goの古いIssueトラッカーやメーリングリストを詳細に調査しないと特定が困難な場合があります。コミットメッセージに記載されている情報に基づいて解説を生成しています。)