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

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

コミット

commit df9f4f14b988c1a6dd0b5106ed1f3720c43fdd28
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Mon Apr 1 21:01:50 2013 +0200

    cmd/gc: do not reuse bool temporaries for composite equality.
    
    Reusing it when multiple comparisons occurred in the same
    function call led to bad overwriting.
    
    Fixes #5162.
    
    R=golang-dev, daniel.morsing
    CC=golang-dev
    https://golang.org/cl/8174047

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

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

元コミット内容

cmd/gc: do not reuse bool temporaries for composite equality.

このコミットは、Goコンパイラのgc(現在のcmd/compileに相当)において、複合型の等価性比較(例: 構造体や配列の比較)を行う際に、一時的に使用されるbool型のテンポラリ変数の再利用方法に関するバグを修正するものです。具体的には、同じ関数呼び出し内で複数の比較が行われた場合に、このテンポラリ変数の再利用が不正な上書きを引き起こし、誤った比較結果を招く問題に対処しています。

変更の背景

Go言語では、構造体や配列などの複合型に対しても等価性比較(==!=)が可能です。これらの比較は、内部的には要素ごとの比較に展開されます。コンパイラは、このような比較を処理する際に、比較結果を一時的に保持するためのテンポラリ変数(一時変数)を生成します。

このコミットが修正する問題は、Goコンパイラの最適化戦略に起因していました。以前のバージョンでは、コンパイラはパフォーマンス向上のため、bool型の一時変数を積極的に再利用していました。しかし、if s := fmt.Sprint(onesA == onesB, onesA != twos, onesB != twos); s != "true true true" のような、単一のステートメント内で複数の複合型比較が連続して行われるケースにおいて、同じ一時変数が複数の比較結果を保持しようとすることで、値が上書きされてしまい、期待される論理結果が得られないというバグが発生していました。

この問題は、GoのIssue #5162として報告されており、特に配列の等価性比較において顕著でした。配列の比較は、その要素数が多くなると、コンパイラが生成するコードも複雑になり、一時変数の管理がより重要になります。このバグは、プログラムの論理的な振る舞いを損なう可能性があり、修正が急務でした。

前提知識の解説

Goコンパイラ (gc)

Goコンパイラは、Go言語のソースコードを機械語に変換するツールです。かつてはgcという名前で知られていましたが、現在はcmd/compileというパッケージ名で提供されています。コンパイラは、ソースコードの字句解析、構文解析、意味解析、中間コード生成、最適化、そして最終的な機械語コード生成といった複数のフェーズを経て動作します。

複合型の等価性比較

Go言語では、プリミティブ型だけでなく、構造体や配列といった複合型も==演算子で比較できます。

  • 構造体: 全てのフィールドが比較可能(等価性比較が定義されている型)であれば、構造体全体を比較できます。全てのフィールドが等しい場合にのみ、構造体は等しいと判断されます。
  • 配列: 同じ型の要素を持ち、同じ長さの配列同士を比較できます。全ての要素が等しい場合にのみ、配列は等しいと判断されます。

これらの複合型の比較は、コンパイラによって個々の要素の比較に分解され、その結果が結合されて最終的な真偽値が決定されます。

テンポラリ変数 (Temporary Variables)

コンパイラは、複雑な式や関数呼び出しの結果を一時的に保持するために、内部的にテンポラリ変数(一時変数)を生成します。これらの変数は、プログラマが明示的に宣言するものではなく、コンパイラがコード生成の過程で必要に応じて作成し、管理します。最適化の一環として、コンパイラはこれらのテンポラリ変数を再利用することで、メモリ使用量を削減したり、レジスタ割り当てを効率化したりすることがあります。しかし、その再利用ロジックに不備があると、今回のようなバグにつながる可能性があります。

walk.c

src/cmd/gc/walk.cは、Goコンパイラのバックエンドの一部であり、中間表現(IR)の「ウォーク」(走査)と変換を行うファイルです。このフェーズでは、抽象構文木(AST)がより低レベルの中間表現に変換され、最適化やコード生成の準備が行われます。特に、式の評価順序の決定、テンポラリ変数の割り当て、および特定の操作(今回の場合は等価性比較)の具体的な実装がここで行われます。

技術的詳細

このコミットの技術的詳細を理解するためには、Goコンパイラが複合型の等価性比較をどのように処理していたかを知る必要があります。

Goコンパイラは、A == Bのような複合型の比較を、内部的には一連の要素ごとの比較と、それらの結果を結合するロジックに変換します。この際、各要素の比較結果や、中間的な論理演算の結果を保持するために、bool型の一時変数(tempbool)が使用されていました。

問題は、walkcompare関数内でtempboolが再利用されるロジックにありました。元のコードでは、比較結果をtempboolに格納し、そのtempboolを直接使用していました。

// 変更前 (簡略化)
walkcompare(...) {
    // ...
    // 比較結果を tempbool に格納
    // ...
    if(n->op == OEQ)
        r = tempbool; // tempbool を直接使用
    else
        r = nod(ONOT, tempbool, N); // tempbool を否定して使用
    // ...
}

このアプローチは、単一の比較式であれば問題ありません。しかし、fmt.Sprint(onesA == onesB, onesA != twos, onesB != twos)のように、同じステートメント内で複数の複合型比較が連続して評価される場合、最初の比較がtempboolに結果を書き込んだ後、次の比較が評価される前に、そのtempboolの値が別の比較によって上書きされてしまう可能性がありました。これにより、後続の比較が誤ったtempboolの値を参照し、最終的な論理結果が不正になるという現象が発生していました。

このコミットの修正は、このtempboolの直接的な再利用を避けることにあります。具体的には、tempboolの値を直接使用するのではなく、tempboolの値を新しい一時変数にコピーしてからその新しい一時変数を使用するように変更されました。

// 変更後 (簡略化)
walkcompare(...) {
    // ...
    // 比較結果を tempbool に格納
    // ...

    // tempbool の値を新しい一時変数 r にコピー
    r = temp(types[TBOOL]); // 新しい bool 型の一時変数 r を作成
    a = nod(OAS, r, tempbool); // r = tempbool; という代入ノードを作成
    typecheck(&a, Etop);
    walkstmt(&a);
    *init = list(*init, a); // 初期化リストに代入を追加

    // 新しい一時変数 r を使用
    if(n->op != OEQ)
        r = nod(ONOT, r, N); // r を否定して使用
    typecheck(&r, Erv);
    walkexpr(&r, init);
    *np = r;
}

この変更により、各比較式はそれぞれ独立した一時変数に結果を保持するようになり、tempboolの不正な上書きによる問題が解消されました。新しい一時変数は、そのアドレスが取られないため、コンパイラの最適化によって最終的にレジスタに割り当てられたり、不要な場合は最適化によって削除されたりする可能性があり、パフォーマンスへの影響は最小限に抑えられます。

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

変更は主に src/cmd/gc/walk.c ファイルの walkcompare 関数内で行われています。

--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -2856,11 +2856,19 @@ walkcompare(Node **np, NodeList **init)\n \ttypecheck(&call, Etop);\n \twalkstmt(&call);\n \t*init = list(*init, call);\n-\t\n-\tif(n->op == OEQ)\n-\t\tr = tempbool;\n-\telse\n-\t\tr = nod(ONOT, tempbool, N);\n+\n+\t// tempbool cannot be used directly as multiple comparison\n+\t// expressions may exist in the same statement. Create another\n+\t// temporary to hold the value (its address is not taken so it can\n+\t// be optimized away).\n+\tr = temp(types[TBOOL]);\n+\ta = nod(OAS, r, tempbool);\n+\ttypecheck(&a, Etop);\n+\twalkstmt(&a);\n+\t*init = list(*init, a);\n+\n+\tif(n->op != OEQ)\n+\t\tr = nod(ONOT, r, N);\
 \ttypecheck(&r, Erv);\
 \twalkexpr(&r, init);\
 \t*np = r;\

また、この修正を検証するための新しいテストケース test/fixedbugs/issue5162.go が追加されています。

コアとなるコードの解説

walkcompare関数は、Goコンパイラが等価性比較(==!=)を処理する際に呼び出される重要な関数です。この関数は、比較対象のノード(np)と、初期化ステートメントのリスト(init)を受け取ります。

変更前のコードでは、複合型の比較結果がtempboolというグローバルな(または関数スコープで再利用される)一時変数に格納された後、そのtempboolが直接r(最終的な比較結果を表すノード)に割り当てられていました。OEQ(等しい)の場合はtempboolをそのまま、それ以外(ONE、等しくない)の場合はtempboolを否定したものをrとしていました。

変更後のコードでは、以下のステップが追加されています。

  1. r = temp(types[TBOOL]);
    • temp関数を呼び出して、新しいbool型の一時変数を生成し、それをrに割り当てます。これにより、tempboolとは異なる、独立した一時変数が確保されます。
  2. a = nod(OAS, r, tempbool);
    • OASは代入演算子を表すノードです。ここでは、「r = tempbool;」という代入操作を表すASTノードaを作成しています。これは、tempboolに格納された比較結果を、新しく作成した一時変数rにコピーする操作です。
  3. typecheck(&a, Etop);
    • 作成した代入ノードaの型チェックを行います。Etopは、ステートメントのトップレベルにある式であることを示します。
  4. walkstmt(&a);
    • 代入ノードaをウォーク(処理)します。これにより、コンパイラは実際にtempboolからrへの値のコピーを行うためのコードを生成します。
  5. *init = list(*init, a);
    • この代入ステートメントaを、現在の初期化ステートメントのリスト*initに追加します。これにより、この代入操作が実際のコード生成時に実行されるようになります。

これらの変更により、tempboolの値が直接再利用されるのではなく、常に新しい一時変数にコピーされてから使用されるようになりました。これにより、同じステートメント内で複数の複合型比較が行われても、それぞれの比較結果が独立して保持され、不正な上書きが防止されます。

test/fixedbugs/issue5162.go は、このバグを再現し、修正が正しく機能することを確認するためのテストファイルです。このテストは、様々なサイズの配列と異なる型(int, uint, floatなど)を使用して、複数の等価性比較を単一のfmt.Sprint呼び出し内で行い、期待される結果("true true true")が得られることを検証しています。特に、CheckEqNNN_TTTExtraVar関数では、追加の変数onesXを導入することで、一時変数の再利用が問題を引き起こすシナリオをより明確にシミュレートしています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (Go言語の型、比較演算子に関する情報)
  • Goコンパイラのソースコード (特に src/cmd/gc/walk.c の周辺コード)
  • Go Issue #5162 の議論スレッド (バグの詳細と修正の背景)
  • Go Gerrit Code Review (変更のレビューコメント)
  • コンパイラの最適化に関する一般的な知識 (一時変数の管理、レジスタ割り当てなど)