コンテンツへスキップ
Topos Uranos

要約

プログラムの中の小数をもつ数は、数学の実数ではない。それは分母が 2 のべきである有理数の有限な部分集合であり、この部分集合に属さない値はすべて、属するもののうち最も近いものに置き換えられる。このレッスンでは、浮動小数点数が 64 ビット(符号、指数、仮数)にどう保持されるか、なぜ 0.10.1 には正確な表現がなく、どの値がその代わりになるのか、float と double では何桁が安全か、丸め誤差がどう蓄積するか、なぜ二つの実数を == で比較せず、根拠のある許容誤差でどう比較するのか、そして収まらない結果や存在しない結果のためにこの形式が予約している無限大と非数とは何かを学ぶ。

学習目標

  1. なぜ 0.10.1 に正確な二進表現がないのか、その代わりにどの値が保持されるのかを説明する。
  2. 繰り返しの和と、大きさが大きく異なる数の和において、丸めがどう影響するかを予測する。
  3. 実数を == ではなく適切な許容誤差で比較し、その許容誤差を選んだ根拠を示す。
  4. 必要な精度に応じて float と double を選び、無限大と非数を見分ける。

2 を底とする科学的記数法

レッスン 整数とその表現 では、nn ビットの整数がちょうど 2n2^n 個の連続する値を表すことを見た。しかし、小数をもつ数には別の難しさがある。どの二つの実数の間にも無限に多くの実数があり、有限個のビットではそれらをすべて区別することはできない。今日のすべてのコンピュータが採用している解決策は、非常に大きな量や非常に小さな量を書くために科学が使うものと同じ、科学的記数法である。10 を底とすると、地球から太陽までの平均距離は 1.496×10111.496 \times 10^{11} メートルと書かれる。この数には符号と、少数の有効数字(仮数、1.4961.496)と、底のべき(指数、1111)があり、指数は小数点の位置を示す。そうであれば、決まった桁数で大きさの桁がまったく異なる量を扱える。その代償は、それぞれの量の最初の数桁しか保持しないことである。

コンピュータは同じことを 2 を底として行う。0 でない数はどれも、ただ一通りに次の形に書ける。

x=(−1)s×1.m1m2m3…m52 (2)×2ex = (-1)^{s} \times 1.m_1 m_2 m_3 \ldots m_{52} \, {}_{(2)} \times 2^{e}

ここで ss は符号(正なら 0、負なら 1)、1.m1m2…1.m_1 m_2 \ldots は二進法で書いた仮数で、その最初の桁は常に 1 であり、ee は指数である。最初の桁が常に 1 であることには実用上の帰結がある。それを保持する必要がなく、節約した 1 ビットを精度に回せるのである。浮動小数点という名前はまさにこの形を表している。小数点は桁の間で決まった位置を占めず、指数に応じて「浮動する」。

C++ の double 型は、現行のすべてのコンパイラで、国際的に標準化された 64 ビットの倍精度二進形式を使う。符号に 1 ビット、指数に 11 ビット、仮数に(最初の桁を除いて)52 ビットである。指数はずらして保持される。11 ビットのフィールドには e+1023e + 1023 が入るので、−1022-1022 から 10231023 までの指数に対して 1 から 2046 までの符号なしの値を保持する(フィールドの値 0 と 2047 は、ゼロ、非常に小さな数、無限大、非数のために予約されており、これらはレッスンの最後で扱う)。float 型は 32 ビットの単精度形式を使い、符号に 1、指数に 8、仮数に 23 ビットを割り当てる。

double のビットは見ることができる。次のプログラムは std::bit_cast(ヘッダ <bit> のもの)を使う。これは数の 64 ビットを、一つも変えずに 64 ビットの符号なし整数として読み直す。ここではこれ以上説明せず、メモリをのぞく道具として使う。書式指定 {:064b} は、前のレッスンと同じく、その整数を 64 桁の二進法で書き、{:016X} は 16 桁の十六進法で書く。

C++ のプログラム
#include <bit>#include <cstdint>#include <print>​int main(){    const double tenth{0.1};    // 数の 64 ビットを符号なし整数として読む。    const std::uint64_t bits{std::bit_cast<std::uint64_t>(tenth)};    std::println("{:064b}", bits);    std::println("{:016X}", bits);}
出力
00111111101110011001100110011001100110011001100110011001100110103FB999999999999A
例0.1 の 64 ビット

図は、プログラムが書いた 64 ビットをまとめ、フィールドごとに読む。

証明

  1. 1+11+52=641 + 11 + 52 = 64

    64 ビットを十六進法で書くと 3FB999999999999A。左から右へ、符号 1 ビット、指数 11 ビット、仮数 52 ビットと読む。

  2. s=0s = 0

    符号は 0 で、数は正である。

  3. e=1019−1023=−4e = 1019 - 1023 = -4

    指数のフィールドは二進法で 0111111101101111111011 で、値は 1019。そこからずらし幅 1023 を引くと、指数は −4-4 である。

  4. m≈1.6000000000000000888m \approx 1.6000000000000000888

    仮数は、保持されない最初の 1 を含めると、二進法で 1.1001100110011…10101.1001100110011\ldots1010 である。10011001 の組が繰り返され、最後の組 10101010 は切り上げられている。その値はおよそ 1.60000000000000008881.6000000000000000888 である。

  5. x=0.1000000000000000055511…≠0.1x = 0.1000000000000000055511\ldots \neq 0.1

    保持される数は +1.6000000000000000888×2−4+1.6000000000000000888 \times 2^{-4}、すなわち 0.1000000000000000055511…0.1000000000000000055511\ldots である。0.10.1 にごく近いが、0.10.1 ではない。