これは何?
数の表現にも色々あって、一長一短だよということをまとめておきたいと思って。
複素数は扱わない。実数の部分集合のみ。
そもそも
そもそも、数は無限にあるけどコンピュータのメモリ(のようなもの)は有限。
有限のコンピュータで無限にあるすべての数を正確に表現することはできない。
なので、表現できる値にはかならず上限と下限がある。
精度にも限りがある。
どんな計算も正確にできるような方法は有限のコンピュータの中に作ることはできない。
コンピュータにおける数の表現の仕方には、用途に合わせて色々な制限の付け方があり、それぞれ一長一短。
種類
適当に分類してみる。
長さ
固定長と可変長がある。
固定長の場合、実際に計算に使う装置(CPU) が得意とする長さかどうかでパフォーマンスが大きく変わる。
値
表現できる値の範囲で色々ある
- 符号なし整数
- 符号付き整数
- 整数以外を含む値
上記で「整数」と書いたけど、数学の整数と違って上限下限がある。
整数以外を含む値には、表現のしかたとして下表の三種類がある
名前 | 持つ値 | 表現する値 |
---|---|---|
有理数 | 整数2つ | 整数 ÷ 整数 |
固定小数点数 | 整数1つ | 整数 × 定数 |
浮動小数点数 | 符号・仮数・指数 | (±1) × 整数 × 定数の整数乗 |
上記で「有理数」と書いたけど、これは数学の有理数のことではない。
数学の「有理数」は無限にあるけど、コンピュータで表現できる有理数は有限なので、おなじになることはない。
表現
昨今のコンピュータの話をする。
いにしえのコンピュータにはもっと違うやり方もあったと思うけど、そういう話はこの記事ではしない。
符号なし固定長整数
基本は 2進数。
32bit を 4bit ずつに分けて、4bit で表現できる 0〜15 のうち 0〜9 を使って 10進数にしたりすることもある。
符号つき固定長整数
こちらも基本は2進数。
普通、MSB側 (大きい桁の側)の端に符号ビットがある。
たとえば 16bit の場合。
ビット | 15 | 14 | … | 1 | 0 |
---|---|---|---|---|---|
符号なし | 32768 | 16384 | … | 2 | 1 |
符号つき | -32768 | 16384 | … | 2 | 1 |
という具合になっていて、最上位ビットの意味だけが符号なしとは異なる。加減は符号なしと同じ計算ですむ。便利。
符号なしよりも扱える値の上限が小さくなるけど、負になれるので便利。
32bit を 1bit(符号) + 15bit + 1bit(使わない) + 15bit に分けて。
15bit で扱える 0〜32767 のうち 0〜9999 を使って 1万進数 ということもあるかもしれない。
可変長整数
CPU が直接扱えない、大きな整数を扱うのが主な用途。
ruby の 整数や JavaScript の BigInt なんかが可変長整数となっている。
可変長にも符号なしがありえるけど、メリットが少ないのか、符号なしは見かけない気がする。
そう簡単にはオーバーフローしないのが便利。
逆に、計算ミスとか予期せぬ入力とかで思いがけない超巨大数になっても計算が続いてしまって DOS 攻撃みたいになることがあってちょっと怖い。
内部表現は、用途によって 10進数だったり 2進数だったりする。
有理数
有理数は、分子の整数と分母の整数という2つの整数の組になっている。
分母は符号なし整数でいいけど、その処理系で符号なし整数を扱うのがめんどくさい場合は符号付き整数で持ったりもする。
整数は固定長でも可変長でもいい。ruby の Rational
なんかは可変長。
固定長の整数で有理数を作ると、すぐに分母または分子がオーバーフローするので、固定長で有理数を作るときには注意深く運用する必要がある。
四則演算だけしている限り(そしてオーバーフローしない限り)誤差なく計算できるのがすごく便利。
しかし、気楽に計算していると 巨大数÷巨大数 という内部表現になっていてめちゃくちゃ遅くなることがあるのがちょっと怖い。
固定小数点数
小数点の位置が動かないという意味で、固定小数点。固定小数点を使った数の表現ということで、固定小数点数。
内部表現は、符号付き整数ひとつ。
「値の意味は、内部表現の値の 100分の1 とする」という具合になっている。この「100分の1」は、ハードコード、つまり固定されていて、実行時には変わらない。
つまり。単位が違うだけで単なる整数と同じという気持ち。
符号なしのこともあり得る。
加減は普通の整数と同じ。
乗算除算はうまくやらないと不用意にオーバーフローしたりする。
あつかう値の範囲がある程度狭い場合に便利。実質的に整数なので、計算も速い。
「±1京までの値が、0.001 単位で表現できれば良い」なら、内部を 64bit 符号付きにして、1000分の1 にすると要求を満たす。
「1000分の1」のように 10進数によりそうとお金の計算に使いやすそうで、「1024分の1」のように 2進数によりそうと計算が速そう。
有効桁数
固定小数点数の有効桁数は可変になる。
たとえば「内部表現の 0.001倍」という固定小数点数の場合。
1000 を 0.01% 増やすことはできるけど、1 を 0.01% 増やすことはできない。
このあたりが気持ち悪いと感じる用途なら固定小数点数を使うべきではない。
浮動小数点数
小数点の位置が動くという意味で、浮動小数点。浮動小数点を使った数の表現ということで、浮動小数点数。
内部表現は 符号・仮数・指数 に分かれる。現在みんなが使っている浮動小数点数は
±0・非正規化数・正規化数・無限・非数
の5つのカテゴリに分かれているけど、この記事では正規化数の話だけをする。
非正規化数については拙記事
が参考になると思う。
浮動小数点数にも色々あり。
基数(2進数なのか10進数なのか、はたまたそれ以外なのか)
長さは固定か可変か。固定ならどれぐらいか
で、用途ごとに適切に使う必要がある。
各種浮動小数点数がどうなっているのかを表にした。
調査対象 | 基数 | 長さ |
---|---|---|
C/C++ の double | 2進数 | 64bitなど(コンパイラ依存) |
Zig の f80 | 2進数 | 80bit |
Java・C# の double | 2進数 | 64bit |
C# の decimal | 10進数 | 128bit |
Java の BigDecimal | 10進数 | 可変長 |
Ruby の BigDecimal | 10進数 (1万進数?) | 可変長 |
Julia の BigFloat | 2進数 | 可変長 |
10進数と 2進数
10進数は、人間がやる 10進数の筆算 と同じ様に誤差が出るのがメリット。
「10進数なので正確だ」とか「誤差がない」とかいうのは誤解。
10進数なので、10進数の筆算 で誤差が出ない計算では誤差が出ないし、10進数の筆算で正確に計算できないときには同じ様に正確に計算ができない。3で割ったら割り切れないのは 2進数でも10進数でも同じ。3進数なら、3で割った場合の誤差がない。
ruby の BigDecimal はたぶん1万進数だけど、これは 1万進数で計算すれば 10進数と同じ様に誤差を出せるからだと思う。
2進数は、コンピュータにとって素直なので計算が速い。
10進数と比べてメモリ効率が良いので、同じ長さなら 10進数より精度が良いけど、10進数で行う筆算とは誤差の出方が違う。
誤差の出方がルールとあっているかどうかが気にならない場合(つまり、入力が気圧とか角度とかの場合)は 2進数が正解。
固定小数点数との比較
浮動小数点数は、固定小数点数とくらべると複雑だけど、扱える値の範囲が広い。
同じ32bit という固定長で表現するとして、下表のようにぜんぜん違う
値の種類 | 最大値 ÷ 絶対値が最小の値 |
---|---|
固定小数点数 | 21億 ぐらい |
浮動小数点数 | 3億×無量大数 ぐらい |
※ 32bit 浮動小数点数にもいろいろあり、上表は一例。
気をつけたいのは、固定小数点数は中間値も固定小数点数だということ。
たとえば平均を取る場合
平均 = \frac{1}{n} \sum_{i=0}^{n-1}{a_i}
という計算をすると思うけど、固定小数点数だと容易にオーバーフローする。オーバーフローが怖くて先に除算するとこんどはアンダーフローする。
浮動小数点数だとだいぶ可能性は低い。絶対大丈夫という事ではないけれど、極めてマシ。
その意味で。
固定小数点数は途中の計算の気遣いが大変。
浮動小数点数はオーバーフロー・アンダーフロー しにくいので気楽。
一方。中身は単なる整数である固定小数点数は計算結果を予期しやすい。
中身が指数部仮数部になっていたり、非正規化数なんていうものも抱えている(場合が多い)浮動小数点数は、複雑で計算結果を予期しにくい。
ということで。
ということで。
規約と同じような丸めかたができるので規約どおりになる、という意味で誤差がないということはあるけど、コンピュータで表現できる数には限りがあることにはどの方法を選んでも変わりがない。
この記事に挙げたいずれの方法でも「フィボナッチ数の $2^{64}$ 番目」や $ \sqrt{2}$ を正確に表現することはできないわけで、その意味で常に制限も誤差もある。限られた範囲のおおよその値しか表現できない。
- 整数だけで良ければ整数型
- 計算途中や理不尽な入力でもマイナスにならないなら符号なし、そうでなければ符号あり。
- 固定長で表現できる範囲を超える心配がなければ固定長。そうでなければ可変長。
- 整数だけだと困るなら
- ファーストチョイスは、使っている処理系が提供する倍精度浮動小数点数(つまり、2進数で固定長)。
- 四則演算での誤差が許されないなら有理数。
- 固定小数点数で(計算途中も含めて)オーバーフロー・アンダーフロー の心配がなければ固定小数点数。
- 扱う値の丸め方が 10進数で規定されていたら 10進数。
- 固定長でなんとかなる精度なら固定長。無理なら可変長。
のような感じ。
良い方法がどれなのかはケースバイケース。
ここに書いてあるような方法ではどれもだめで、独自の方法を編み出す必要があることもある。