3 ポイント 投稿者 GN⁺ 2025-09-13 | 1件のコメント | WhatsAppで共有
  • この記事は、浮動小数点(float) 値がどのように メモリ上に保存 され、表現されるかを説明する
  • 値の 16進数・10進数の形式 と、実際の数値への変換方法に焦点を当てる
  • 符号(Sign)、指数(Exponent)、仮数(Significand) 領域の定義と、それぞれの役割を説明する
  • 特定の float 値が 正確にどのような2進数・10進数の値 を表すのかを解釈する方法の例を含む
  • 表現可能な値どうしの 差分(Delta) の計算 にも触れている

浮動小数点値の保存構造の分析

  • "halfb float float double" など、さまざまな 浮動小数点フォーマット が存在する
  • 各値は、Raw Hexadecimal Integer Value(16進整数値)Raw Decimal Integer Value(10進整数値) のようにメモリ内の保存値として確認できる
  • 16進データは Hexadecimal Form ("%a") として、実際の浮動小数点表記に結び付けられる
  • 各値の位置は Significand–Exponent Range(仮数–指数範囲上の位置) として示される

2進数および10進数の値の解釈方法

  • 浮動小数点数は Base-2(2進法の評価式) として次のように表せる:
    • (−12)02×​102(100010012 − 011111112)​×​1.011111110010100000000002
      → 2進表現式による数値評価
  • Base-10(10進法の評価式) では次の形になる:
    • 1×​210×​1.4967041015625
      → 2の10乗と小数部分の積として表現する
  • 実際に変換した際の 正確な10進数値 も表示される:
    • 1.532625×​103 のような形で示される

隣接する値との距離(Delta)の計算

  • 表現可能な値のあいだの Delta(間隔) は重要な意味を持つ
  • 次(Next)または前(Previous)の 表現可能な値までの距離(Delta to Next/Previous Representable Value) をそれぞれ提供する
    • 例: ±1.220703125×​10-4
  • この間隔は、浮動小数点値の 有効桁数/精度 と関係している

要約

  • 浮動小数点の メモリ表現 と、2進・10進変換の原理
  • sign, exponent, significand 構造の説明
  • 表現範囲と隣接値との間隔情報 もあわせて整理している

1件のコメント

 
GN⁺ 2025-09-13
Hacker Newsの意見
  • このテーマについては、この説明が最高だと思う: https://fabiensanglard.net/floating_point_visually_explained/ Hacker Newsを始めた頃にこの記事に出会って、こういう内容がこのプラットフォームに残り続けてほしいと思う動機になった: https://news.ycombinator.com/item?id=29368529

    • 数学寄りすぎるように見えるかもしれないけど、その説明はそこまで簡単ではなかった 浮動小数点について本当に簡単な説明をするなら、スケールに関係なくおおむね同じビット数の精度を提供するということ つまり、1よりはるかに小さい数でも、1の近くでも、非常に大きな数でも、先頭側のビットについてほぼ同じ程度の精度が期待できる これが中核となる性質だが、直感的に身につけるのは簡単ではない

    • 最近TM研究チームが書いたブログとも文脈がよく合っている https://news.ycombinator.com/item?id=45200925

    • こんなにうまく説明されたものは見たことがなかったので、共有してくれてありがたい

  • 私が長いこと悩んでいた問題の一つが、「float値を最短かつ明確な10進文字列で表現する方法」だった たとえば単精度floatを使う場合、floatを一意に識別するには最大9桁の10進精度が必要になる だから %.9g のようなprintfパターンを使う必要がある しかしその場合、0.1が0.100000001のような見苦しい値で出力される そこで普通は6桁に丸めて表現するが、%.6g を使えば6桁まで入力された10進値は保存された値と同じように出力できる ただし計算結果として得られた値については、round-tripが安全ではなくなる とくにfloat値を正確に比較しなければならないとき(たとえばデータ変更の有無を確認するとき)には、この点が重要になる 私が考えたアイデアは、まず6桁で出力してみて、それをパースしたときに同じバイナリ値になるならそれを使い、違うなら7桁、8桁、9桁まで繰り返して最短の10進表現を見つけるというものだった 私のアルゴリズムは以下の通り

    int out_length;
    char buffer[32];
    for (int prec = 6; prec<=9; prec++) {
      out_length = sprintf(buffer, "%.*g", prec, floatValue);
      if (prec == 9) {
        break;
      }
      float checked_number;
      sscanf(buffer, "%g", &checked_number);
      if (checked_number == floatValue) {
        break;
      }
    }
    

    printf/scanfを繰り返さずに、より効率的に最短表現を見つける方法があるのか気になる

    • この問題は実際に重要だ (最も近い表現になるという条件のもとで)特定のfloatを「正規化された」文字列にする問題と見なせる そのため、Dragon4、Grisu3、Ryu、Dragonboxのようなさまざまな効率的アルゴリズムがある Googleのdouble-conversionライブラリも最初の二つを実装している

    • printf/scanfループなしで求めるもっと良い方法がある printf("%f", ...) だけでも可能だ floatからstringへの変換の実際のアルゴリズムはかなり複雑だ 最近の優れたアルゴリズムは https://github.com/ulfjack/ryu だ これよりさらに効率的な方法が最近出たはずだが、名前は思い出せない

    • 否定的な意見はあまり気にしなくていい、たとえ最良の方法ではなくても(エラーがないなら)たいてい十分うまく動く 実際、私も似たような経験があって、あるときオイラー回転 (5°, 5°, 0) の後に同じベクトルになるベクトルを見つけたくて、ランダムにベクトルを少しずつ動かしながら基準ベクトルに近づくかを見ていた 数百万回ループを回し、Pythonで数秒以内に結果を得られた ライブラリレベルでは非効率かもしれないが、自分の用途には大いに満足だった

    • std::numeric_limits<float>::max_digits10 を参照するとよい https://en.cppreference.com/w/cpp/types/numeric_limits/max_digits10.html

    • 無意味だし、sscanf() は絶対に使うべきではない 符号なし整数に変換してシリアライズ/復元すれば、情報損失なしで可逆になる

      double f = 0.0/0.0; // コンパイラによっては soft error フラグが必要かもしれない
      double g;
      char s[9];
      
      assert(sizeof double == sizeof uint64_t);
      
      snprintf(s, 9, "%0" PRIu64, *(uint64_t *)(&f));
      
      snscanf(s, 9, "%0" SCNu64, (uint64_t *)(&g));
      

      もっと短い表現が必要なら、元に戻せるヒューリスティックを使えばよいが、元の精度が保証される方法であるべきだ(たとえば冪等性)

  • 私がいちばん好きなFP関連の豆知識は、float比較がほとんど整数比較のように使えるという点だ a > b を判定するとき、a、bを符号付き整数として解釈してそのまま比較すればよい この方法は(ほぼ)うまく動く つまり、次に大きいfloat値はビットパターンを整数に変換して1を足したものだ たとえば、0.0 floatから始めて整数加算で1を足せば、それが次のfloat値になる(denormal、最小の隙間の値) この原理で nextafter も実装される float値が整数比較の順序と同じだと分かると、ずっと自然に感じられる もちろん例外はある: NaN、無限大、負のゼロなどは異なる 使いどころはいくつかあるが、万能ではない

    • これは厳密には正しくない 正の数同士、あるいは正と負の比較では合っているが、負の数同士の比較は異なる 標準的な浮動小数点(float)は sign-magnitude 方式で、現代の符号付き整数は2の補数だ 負の数では両者の大小比較の向きが逆になる floatをintのように1ずつ増やすと、通常は同じ符号の範囲内で「大きさ」がより大きい数へ移動する つまり正の数は上がり、負の数はより小さい負の数の側へ下がる 整数では常に上がるか、オーバーフローする より正確に言うなら、sign-magnitude整数比較と同じと言える もちろん、先に挙げた caveat は依然として有効だ

    • ちなみにRust標準ライブラリの、NaNも比較可能なtotal-order浮動小数点大小比較アルゴリズムは以下の通り(IEEE 751推奨)

      let mut left = self.to_bits() as i32;
      let mut right = other.to_bits() as i32;
      
      // 負数のとき、符号を除くすべてのビットを反転させると
      // 2の補数整数比較に似た構造で整列される
      
      left ^= (((left >> 31) as u32) >> 1) as i32;
      right ^= (((right >> 31) as u32) >> 1) as i32;
      
      left.cmp(&right)
      

      アルゴリズム全体を見る

  • この話題には、私のOMSCSゲームAIコースで、浮動小数点でゲームオブジェクトの位置を表現するときの注意点を扱った事例で触れた 原点や参照点から遠ざかるほど、floatはより大きな値を保持するために精度が落ちるので危険だ

    • こういう現象がMinecraftの神話のようにFar Landsとして定着したのが面白い つまり、ワールドの原点から遠くへ行くほど地形生成や物理挙動が少しずつおかしくなり、さらに遠くへ行くと完全に壊れる 少しオカルトめいていて、現実の法則が徐々に崩れていくような現象だ これがすべてfloat精度の限界によるものだ

    • floatの0〜1の間の数をたくさん足すとき、単純に順番に足す方法と、二つずつ組にして足してからまた合計する方法(ペアリング)を比べると、ペアリングのほうがずっと正確だ floatの累積誤差の影響が深刻であることを示す例だ 実際、こうしたfloat誤差が軽視されて問題になった事例もあった Donald Knuthの "The Art of Computer Programming" では、この点や、a + (b + c) ≠ (a + b) + c といったfloatの基本的な真実が説明されている 現実でも、実数の扱いの誤りで問題になった例があり、Patriotミサイルシステムは時間の累積をfloatで処理していたため、誤差が次第に積み上がって目標を大きく外し、再起動が必要になった 24時間ごとの再起動が必要で、最終的にはシステムソフトウェアが改善された float誤差によって大きな構造物が崩壊することもある(厚みの数値が薄すぎると計算されるなど)

    • 境界条件を先に定義して、どの程度の精度が必要かの基準を決めるべきだ そうすれば最小/最大距離も事前に算出できる ワールドが大きすぎるなら、セクターに分けたり、グローバル/ローカル座標を別々に管理したりする必要がある(例: No Man's Sky) ゲームはあくまで舞台装置のようなものだ Double-Precisionならたいていの状況には十分だ 大事なのは、小さい値と大きい値を一緒に足さないようにすることだ

    • Kerbal Space Programでは、32bit floatだけで太陽系を実装しようとして、かなり賢いエンジニアリングが投入されている 関連する記事や動画が多く、とてもおすすめだ

  • この可視化は面白いし、以前私がネットワーク範囲の理解を助けるために作った CIDR range calculator と見た目が似ていて興味深い こうした可視化は非常に役に立つ

  • 昔はfloat表現を調べるときに https://www.h-schmidt.net/FloatConverter/IEEE754.html を使っていた このサイトは変換誤差も見せてくれるのが利点だが、double precisionには対応していない

    • 私もすでに誰かが触れているかコメントをざっと見たが、本当に良いWebページだ ただ、OPで紹介されているサイトは、数値空間の分割構造をグラフで本当に直感的に説明してくれる 縦軸は対数スケールで、横軸は各行ごとに線形だが、対数区間に合わせて正規化されている floatを気楽に理解できる人には当たり前かもしれないが、初学者には補足説明が必要な部分だ
  • まだこのコメント欄では共有されていないが、float関連で私がいちばん好きなサイトは https://0.30000000000000004.com/

  • 32bit floatで「最も興味深い整数」は 16777217(64ビットでは 9007199254740992)という説がある こういう edge case をテストで知っておくと面白い

    • 64ビットfloatでは 9007199254740991 がJavaScriptの Number.MAX_SAFE_INTEGER だ この値は偶数ではなく、次の値 9007199254740992 もそれ自体は安全な値だが、9007199254740993 のような明らかに安全でない値は丸められて区別できなくなるという性質がある

    • 64ビットfloatでは正確には ±9,007,199,254,740,993.0 だ :-) ちなみにこうした値は、floatが「正確に」表現できる最大整数の限界のすぐ次の値を意味する たとえば32ビットfloatでは、±16,777,216.0 の次に表現可能な値は ±16,777,218.0 だ ±16,777,217.0 は表現できないので、通常はゼロ方向などへ丸められる こうした精度限界や丸めの問題は、しばしば見落とされる領域だ

  • IEEE754が存在することは喜ばしいが、IEEE754は完璧ではなく、positのような表現のほうが(ハードウェアサポートがない前提なら)優れていると思う bignum rational(有理数)はそのどちらよりも優れているが、速度は最も遅い

    • IEEE754はさまざまな要求を満たすための妥協案だ 代替方式の中には特定分野で優れているものもあるが、別の分野では劣る
  • 最近GPUで導入されたさまざまなfp8フォーマットにも対応してくれると本当に素晴らしいと思う