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

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

このコミットは、Goコンパイラの6c (ARMアーキテクチャ向け) および 8c (x86アーキテクチャ向け) のコードジェネレータにおける浮動小数点数比較の挙動を、NaN (Not a Number) に対して安全にするための変更を導入しています。具体的には、浮動小数点数の等価性 (==) および非等価性 (!=) の比較において、NaNが関与した場合に予期せぬ結果を避けるためのコード生成ロジックが修正されています。

コミット

commit 109a9763550aac3071e30f6e13cb5ec1172aa017
Author: Russ Cox <rsc@golang.org>
Date:   Thu Jan 26 16:23:29 2012 -0500

    6c, 8c: make floating point code NaN-safe
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/5569071

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

https://github.com/golang/go/commit/109a9763550aac3071e30f6e13cb5ec1172aa017

元コミット内容

このコミットの元のメッセージは以下の通りです。

"6c, 8c: make floating point code NaN-safe"

これは、6c (ARM) と 8c (x86) の両コンパイラにおいて、浮動小数点数に関するコードがNaNに対して安全になるように修正されたことを示しています。

変更の背景

浮動小数点数演算において、NaN (Not a Number) は特殊な値であり、その比較挙動は通常の数値とは異なります。IEEE 754浮動小数点数標準では、NaNは自分自身を含め、いかなる値とも等しくないと定義されています。つまり、NaN == NaNfalse となり、NaN != NaNtrue となります。

従来のコンパイラの実装では、!(l == r) のような論理否定を含む比較が l != r に単純に書き換えられることがありました。しかし、浮動小数点数、特にNaNが関与する場合、この書き換えは問題を引き起こします。

例:

  • l = NaN, r = NaN の場合:

    • l == rfalse
    • !(l == r)true
    • l != rtrue このケースでは問題ありません。
  • l = NaN, r = 5.0 の場合:

    • l == rfalse
    • !(l == r)true
    • l != rtrue このケースでも問題ありません。

問題となるのは、コンパイラが比較演算を最適化する際に、NaNの特殊な挙動を考慮せずに論理的な等価性を仮定してしまう場合です。特に、OEQ (等しい) や ONE (等しくない) のような比較演算子を扱う際に、NaNの特性を正しく反映しないコードが生成される可能性がありました。このコミットは、このような潜在的なバグを修正し、浮動小数点数比較がNaNに対して常に正しい結果を返すようにすることを目的としています。

前提知識の解説

1. IEEE 754 浮動小数点数標準

現代のほとんどのコンピュータシステムで採用されている浮動小数点数演算の国際標準です。この標準は、浮動小数点数の表現形式(単精度、倍精度など)、演算規則、そして特殊な値(無限大 Infinity、非数 NaN)の扱いを定義しています。

2. NaN (Not a Number)

「非数」と訳され、0による除算、無限大同士の減算、負の数の平方根など、数学的に未定義または表現不可能な演算結果を表すために使用されます。NaNには「シグナリングNaN」と「クワイエットNaN」の2種類がありますが、ここではその詳細には触れません。重要なのは、NaNが持つ以下の特性です。

  • 比較の特殊性: NaNは、自分自身を含め、いかなる値とも等しくありません。
    • NaN == X は常に false (XがNaNであっても)
    • NaN != X は常に true (XがNaNであっても)
    • NaN < X, NaN > X, NaN <= X, NaN >= X はすべて false (順序付け不能)

3. コンパイラのコード生成

コンパイラは、プログラマが書いた高水準言語のコードを、コンピュータが直接実行できる機械語に変換します。この変換プロセスの一部として、「コードジェネレータ」が中間表現から最終的な機械語を生成します。この際、最適化が行われることが多く、例えば論理演算の書き換えなどが含まれます。

4. 6c8c

これらはGo言語の初期のコンパイラツールチェーンの一部です。

  • 6c: ARMアーキテクチャ向けのGoコンパイラのコードジェネレータ。
  • 8c: x86アーキテクチャ向けのGoコンパイラのコードジェネレータ。 Go言語はクロスコンパイルをサポートしており、これらのツールは異なるアーキテクチャ向けのバイナリを生成するために使用されます。

5. boolgen 関数

このコミットで変更されている boolgen 関数は、コンパイラのコードジェネレータ内で、ブール式(条件式)を評価し、それに基づいて分岐命令を生成する役割を担っています。例えば、if (a == b) のような条件文は、boolgen によって適切な比較命令と条件分岐命令に変換されます。

技術的詳細

このコミットの核心は、boolgen 関数における浮動小数点数比較のコード生成ロジックの変更です。特に、OEQ (等しい) と ONE (等しくない) の比較演算子に焦点を当てています。

従来のコンパイラでは、!(l == r) のような式を l != r に単純に変換する最適化が行われることがありました。しかし、NaNの特性(NaN == NaNfalseNaN != NaNtrue)を考慮すると、この変換は常に正しいとは限りません。

例えば、l == rfalse と評価される場合、!(l == r)true となります。しかし、l != rtrue と評価されるため、一見すると問題ないように見えます。 しかし、NaNが関与する比較では、l == rfalse となるケースが複数存在します。

  • l = 1.0, r = 2.0 の場合: l == rfalsel != rtrue
  • l = NaN, r = 1.0 の場合: l == rfalsel != rtrue
  • l = NaN, r = NaN の場合: l == rfalsel != rtrue

問題は、コンパイラが OEQONE のような比較命令を生成する際に、NaNの特殊な挙動を考慮せずに、通常の数値比較と同じように扱ってしまう可能性があった点です。

このコミットでは、typefd[l->type->etype] (浮動小数点型であるかどうかのチェック) が真であり、かつ比較演算子が OEQ または ONE である場合に、特別な処理を導入しています。

OEQ (等しい) の場合

OEQ の比較(例: l == r)では、AJEQ (Jump if Equal) 命令が生成されます。しかし、NaNが関与する場合、l == rfalse となるため、AJEQ は常にジャンプしません。NaN-safeにするためには、l == rtrue となる条件(つまり、両辺がNaNでなく、かつ等しい場合)と、l == rfalse となる条件(NaNが関与する場合を含む)を区別する必要があります。

変更後、OEQ の比較では、AJEQ 命令に加えて AJPC (Jump if Parity Clear) 命令が使用されるようになりました。浮動小数点数の比較では、結果フラグにパリティフラグが設定されることがあります。AJPC は、比較結果が順序付け可能(つまりNaNではない)で、かつ等しくない場合にジャンプします。この組み合わせにより、NaNが関与する場合でも正しい分岐ロジックが実現されます。

ONE (等しくない) の場合

ONE の比較(例: l != r)では、AJNE (Jump if Not Equal) 命令が生成されます。NaNの特性により、NaN != X は常に true となります。

変更後、ONE の比較では、AJNE 命令に加えて AJPS (Jump if Parity Set) 命令が使用されるようになりました。AJPS は、比較結果が順序付け不能(つまりNaNである)場合にジャンプします。これにより、l != rtrue となる条件(両辺が等しくない場合、またはNaNが関与する場合)を正確に表現できるようになります。

!(l == r) の書き換え問題の回避

特に注目すべきは、if(true && typefd[l->type->etype] && (o == OEQ || o == ONE)) ブロック内の変更です。 // Cannot rewrite !(l == r) into l != r with float64; it breaks NaNs. というコメントが追加され、!(l == r)l != r に単純に書き換えることがNaNに対して問題を引き起こすことが明示されています。このため、このようなケースでは、boolgen(n, 0, Z) を呼び出して元の式を評価し、その結果に基づいて OGOTO (無条件ジャンプ) を使用して分岐を制御するロジックが導入されています。これにより、NaNの特殊な比較挙動が正しく扱われるようになります。

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

変更は主に src/cmd/6c/cgen.csrc/cmd/8c/cgen.cboolgen 関数内で行われています。

src/cmd/6c/cgen.c の変更点

--- a/src/cmd/6c/cgen.c
+++ b/src/cmd/6c/cgen.c
@@ -1237,11 +1237,12 @@ void
 boolgen(Node *n, int true, Node *nn)
 {
  int o;
- Prog *p1, *p2;
+ Prog *p1, *p2, *p3; // p3が追加
  Node *l, *r, nod, nod1;
  int32 curs;
 
  if(debug['g']) {
+  print("boolgen %d\n", true); // デバッグ出力の追加
   prtree(nn, "boolgen lhs");
   prtree(n, "boolgen");
  }
@@ -1353,6 +1354,15 @@ boolgen(Node *n, int true, Node *nn)
  case OLO:
  case OLS:
   o = n->op;
+  if(true && typefd[l->type->etype] && (o == OEQ || o == ONE)) {
+   // Cannot rewrite !(l == r) into l != r with float64; it breaks NaNs.
+   // Jump around instead.
+   boolgen(n, 0, Z);
+   p1 = p;
+   gbranch(OGOTO);
+   patch(p1, pc);
+   goto com;
+  }
   if(true)
    o = comrel[relindex(o)];
   if(l->complex >= FNX && r->complex >= FNX) {
@@ -1367,6 +1377,10 @@ boolgen(Node *n, int true, Node *nn)
    break;
   }
   if(immconst(l)) {
+   // NOTE: Reversing the comparison here is wrong
+   // for floating point ordering comparisons involving NaN,
+   // but we don't have any of those yet so we don't
+   // bother worrying about it.
    o = invrel[relindex(o)];
    /* bad, 13 is address of external that becomes constant */
    if(r->addable < INDEXED || r->addable == 13) {
@@ -1388,10 +1402,11 @@ boolgen(Node *n, int true, Node *nn)
    cgen(r, &nod1);
    gopcode(o, l->type, &nod, &nod1);
    regfree(&nod1);
-   } else
+   } else { // elseブロックが追加
    gopcode(o, l->type, &nod, r);
+   }
    regfree(&nod);
-   goto com;
+   goto fixfloat; // fixfloatラベルへのジャンプに変更
   }
   regalloc(&nod, r, nn);
   cgen(r, &nod);
@@ -1406,6 +1421,33 @@ boolgen(Node *n, int true, Node *nn)
   } else
    gopcode(o, l->type, l, &nod);
   regfree(&nod);
+ fixfloat: // 新しいラベル
+  if(typefd[l->type->etype]) {
+   switch(o) {
+   case OEQ:
+    // Already emitted AJEQ; want AJEQ and AJPC.
+    p1 = p;
+    gbranch(OGOTO);
+    p2 = p;
+    patch(p1, pc);
+    gins(AJPC, Z, Z);
+    patch(p2, pc);
+    break;
+
+   case ONE:
+    // Already emitted AJNE; want AJNE or AJPS.
+    p1 = p;
+    gins(AJPS, Z, Z);
+    p2 = p;
+    gbranch(OGOTO);
+    p3 = p;
+    patch(p1, pc);
+    patch(p2, pc);
+    gbranch(OGOTO);
+    patch(p3, pc);
+    break;
+   }
+  }
 
  com:
   if(nn != Z) {

src/cmd/8c/cgen.c の変更点

8c の変更も 6c と同様のロジックが適用されています。

--- a/src/cmd/8c/cgen.c
+++ b/src/cmd/8c/cgen.c
@@ -1221,7 +1221,7 @@ void
 boolgen(Node *n, int true, Node *nn)
 {
  int o;
- Prog *p1, *p2;
+ Prog *p1, *p2, *p3; // p3が追加
  Node *l, *r, nod, nod1;
  int32 curs;
 
@@ -1346,6 +1346,15 @@ boolgen(Node *n, int true, Node *nn)
    cgen64(n, Z);
    goto com;
   }
+  if(true && typefd[l->type->etype] && (o == OEQ || o == ONE)) {
+   // Cannot rewrite !(l == r) into l != r with float64; it breaks NaNs.
+   // Jump around instead.
+   boolgen(n, 0, Z);
+   p1 = p;
+   gbranch(OGOTO);
+   patch(p1, pc);
+   goto com;
+  }
   if(true)
    o = comrel[relindex(o)];
   if(l->complex >= FNX && r->complex >= FNX) {
@@ -1378,6 +1387,30 @@ boolgen(Node *n, int true, Node *nn)
    } else
     fgopcode(o, l, &fregnode0, 0, 1);
   }
+  switch(o) { // 新しいswitch文
+  case OEQ:
+   // Already emitted AJEQ; want AJEQ and AJPC.
+   p1 = p;
+   gbranch(OGOTO);
+   p2 = p;
+   patch(p1, pc);
+   gins(AJPC, Z, Z);
+   patch(p2, pc);
+   break;
+
+  case ONE:
+   // Already emitted AJNE; want AJNE or AJPS.
+   p1 = p;
+   gins(AJPS, Z, Z);
+   p2 = p;
+   gbranch(OGOTO);
+   p3 = p;
+   patch(p1, pc);
+   patch(p2, pc);
+   gbranch(OGOTO);
+   patch(p3, pc);
+   break;
+  }
   goto com;
  }
  if(l->op == OCONST) {

コアとなるコードの解説

1. Prog *p3; の追加

Prog はコンパイラの中間表現における命令(プログラム)を表す構造体です。p3 が追加されたのは、新しい分岐ロジックで3つのパッチポイント(ジャンプ先を後で埋めるための場所)が必要になったためです。

2. print("boolgen %d\n", true); の追加

デバッグ目的で、boolgen 関数が呼び出された際の true 引数の値を出力する行が追加されました。これは、条件式の評価方向(真のパスか偽のパスか)を追跡するのに役立ちます。

3. !(l == r) の書き換え回避ロジック

  if(true && typefd[l->type->etype] && (o == OEQ || o == ONE)) {
   // Cannot rewrite !(l == r) into l != r with float64; it breaks NaNs.
   // Jump around instead.
   boolgen(n, 0, Z); // 元の式を「偽」の条件で評価
   p1 = p;
   gbranch(OGOTO); // 無条件ジャンプ命令を生成
   patch(p1, pc); // ジャンプ先を現在のプログラムカウンタにパッチ
   goto com; // 共通の終了処理へ
  }

このブロックは、浮動小数点数 (typefd[l->type->etype]) の等価性 (OEQ) または非等価性 (ONE) の比較において、true パス(条件が真の場合のコード生成)を処理する際に適用されます。 コメントにあるように、!(l == r)l != r に単純に書き換えることがNaNに対して問題を引き起こすため、この最適化を回避しています。代わりに、元の式 nboolgen(n, 0, Z) として「偽」の条件で評価し、その結果に基づいて無条件ジャンプ (OGOTO) を生成することで、NaN-safeな分岐を実現しています。

4. immconst(l) ブロック内のコメント追加

   // NOTE: Reversing the comparison here is wrong
   // for floating point ordering comparisons involving NaN,
   // but we don't have any of those yet so we don't
   // bother worrying about it.

このコメントは、定数との比較において比較の順序を反転させる最適化(例: X < 55 > X に)が、NaNを含む浮動小数点数の順序比較では誤りである可能性を指摘しています。しかし、この時点ではそのようなケースは発生しないため、問題視されていないことが示されています。これは、将来的なNaN関連のバグ修正の可能性を示唆しています。

5. fixfloat ラベルと新しい分岐ロジック

6c のコードでは、gopcode の呼び出し後に goto com; だった箇所が goto fixfloat; に変更され、新しい fixfloat ラベルが追加されています。8c では、fgopcode の呼び出し後に直接新しい switch 文が追加されています。

この fixfloat (または 8cswitch) ブロックが、NaN-safeな浮動小数点数比較の核心です。

case OEQ: (等しい)

   case OEQ:
    // Already emitted AJEQ; want AJEQ and AJPC.
    p1 = p;
    gbranch(OGOTO); // 無条件ジャンプを生成
    p2 = p;
    patch(p1, pc); // p1のジャンプ先を現在のPCに設定
    gins(AJPC, Z, Z); // AJPC命令を生成
    patch(p2, pc); // p2のジャンプ先を現在のPCに設定
    break;

OEQ の比較では、既に AJEQ (Jump if Equal) 命令が生成されています。しかし、NaNの特性により NaN == NaNfalse となるため、AJEQ だけでは不十分です。 このコードは、AJEQ に加えて AJPC (Jump if Parity Clear) 命令を組み合わせることで、NaN-safeな等価性比較を実現しています。

  • AJEQ: オペランドが等しい場合にジャンプ。
  • AJPC: 比較結果が順序付け可能(NaNではない)で、かつ等しくない場合にジャンプ。 この組み合わせにより、両辺が等しい場合(NaNではない)と、NaNが関与して等しくない場合の両方を正しく処理できます。

case ONE: (等しくない)

   case ONE:
    // Already emitted AJNE; want AJNE or AJPS.
    p1 = p;
    gins(AJPS, Z, Z); // AJPS命令を生成
    p2 = p;
    gbranch(OGOTO); // 無条件ジャンプを生成
    p3 = p;
    patch(p1, pc); // p1のジャンプ先を現在のPCに設定
    patch(p2, pc); // p2のジャンプ先を現在のPCに設定
    gbranch(OGOTO); // 無条件ジャンプを生成
    patch(p3, pc); // p3のジャンプ先を現在のPCに設定
    break;

ONE の比較では、既に AJNE (Jump if Not Equal) 命令が生成されています。 このコードは、AJNE に加えて AJPS (Jump if Parity Set) 命令を組み合わせることで、NaN-safeな非等価性比較を実現しています。

  • AJNE: オペランドが等しくない場合にジャンプ。
  • AJPS: 比較結果が順序付け不能(NaNである)場合にジャンプ。 この組み合わせにより、両辺が等しくない場合(NaNではない)と、NaNが関与して常に等しくない場合の両方を正しく処理できます。

これらの変更により、Goコンパイラは浮動小数点数の比較において、IEEE 754標準で定義されたNaNの特殊な挙動を正しく扱うようになり、より堅牢なコードを生成できるようになりました。

関連リンク

参考にした情報源リンク

  • IEEE 754 浮動小数点数標準:
  • 浮動小数点数とNaNの比較挙動に関する一般的な情報源:
    • "What Every Computer Scientist Should Know About Floating-Point Arithmetic" by David Goldberg: https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html (浮動小数点数に関する古典的な論文)
    • 各種プログラミング言語の浮動小数点数に関するドキュメント(例: Java, C#, Pythonなど)
  • コンパイラのコード生成に関する一般的な情報源:
    • "Compilers: Principles, Techniques, and Tools" by Aho, Lam, Sethi, Ullman (通称 Dragon Book)
    • 各種コンパイラのソースコード(例: GCC, LLVM)
    • アセンブリ言語の命令セットリファレンス(x86, ARMなど)
    • (Goコンパイラの内部構造に関する具体的なドキュメントは、Goのソースコード自体や関連する設計文書を参照する必要があります。)