浮動小数点比較の落とし穴

浮動小数点数の比較における一般的な落とし穴と正しい手法を学びます:イプシロン比較、相対許容度、ULPベースの手法。

Precision

Decimal Value

0.3

Float32 Hex

0x3E99999A

Float64 Hex

0x3FD3333333333333

詳細な説明

浮動小数点数を等価性で比較することは、ソフトウェアにおけるバグの最も一般的な原因の1つです。根本的な問題は、多くの正確な10進値がバイナリ浮動小数点で正確に格納できず、算術演算が丸め誤差を導入することです。

素朴な比較の罠:

// これはほぼ常に間違い:
if (a === b) { ... }

// 計算された値に対してもこれは問題あり:
if (result === 0.3) { ... }

方法1: 絶対イプシロン

function nearlyEqual(a, b, epsilon = 1e-10) {
  return Math.abs(a - b) < epsilon;
}

ゼロ付近の値では機能しますが、大きな値では失敗します。x = 1e20では、隣接する浮動小数点数のギャップは約16384なので、1e-10のイプシロンでは等しいとみなすべき2つの値がマッチしません。

方法2: 相対イプシロン

function nearlyEqual(a, b, tolerance = Number.EPSILON * 10) {
  const diff = Math.abs(a - b);
  const norm = Math.max(Math.abs(a), Math.abs(b));
  return diff < norm * tolerance;
}

値の大きさに応じてスケールします。ただし、ゼロ付近では相対誤差が無限大になるため失敗します。

方法3: 複合アプローチ

function nearlyEqual(a, b, relTol = 1e-9, absTol = 1e-12) {
  const diff = Math.abs(a - b);
  if (diff < absTol) return true; // ゼロ付近のケースを処理
  const norm = Math.max(Math.abs(a), Math.abs(b));
  return diff < norm * relTol;
}

これはPythonのmath.isclose()やNumPyのnumpy.allclose()で使用されているアプローチです。

方法4: ULPベースの比較

2つの値が何個の表現可能な浮動小数点数離れているかを比較します:

function ulpDistance(a, b) {
  const buf = new ArrayBuffer(8);
  const f64 = new Float64Array(buf);
  const i64 = new BigInt64Array(buf);
  f64[0] = a; const ai = i64[0];
  f64[0] = b; const bi = i64[0];
  return Number(ai > bi ? ai - bi : bi - ai);
}

1〜4 ULP以内の2つの値は、単一の算術演算では通常等しいとみなされます。

覚えておくべき特殊ケース:

  • NaN !== NaN(NaNは自分自身を含め何とも等しくない)
  • +0 === -0(ただしObject.isで区別可能)
  • Infinity === Infinity(Infinityは自分自身と等しい)
  • ほぼ等しい値の減算は壊滅的な桁落ちを引き起こす

ユースケース

正しい浮動小数点比較は、ユニットテストのアサーション(expect.toBeCloseTo)、金融計算、物理シミュレーション、反復アルゴリズムの収束チェック、および10進値を計算して比較するコードに不可欠です。

試してみる — IEEE 754 Inspector

フルツールを開く