浮動小数点型
前の章では、整数の算術演算は使いやすいものの、オーバーフローや切り捨てによるプログラミングエラーが発生しやすいことを学んだ。また、整数は1.25のように小数部分を持つ値を持つことができないことも学んだ。
浮動小数点型は、小数部分をサポートするように設計されている。この名前の"点"は、整数部分と小数部分を区切る基数点からきており、"浮動"は、これらの型が実装される方法の詳細、つまり小数点が適宜左右に移動することを指している。(この詳細は、これらの型を使用する場合、重要ではない。)
この章でも、重要な詳細について説明しなければならない。その前に、浮動小数点型の興味深い点をいくつか挙げておこう。
- 0.001を1000回加算しても、1を加算した結果とは異なる。
- 浮動小数点型で論理演算子
==および!=を使用することは、ほとんどの場合、誤りだ。 - 浮動小数点型の初期値は0ではなく、
.nanだ。.nanは、式では意味のある方法で使用することはできない。比較演算で使用した場合、.nanはどの値よりも小さくも大きくもない。 - 2つのオーバーフロー値は、
.infinityと負の.infinityである。
浮動小数点型は、場合によってはより便利だが、すべてのプログラマが知っておくべき特殊性がある。整数と比較すると、小数点を扱うことを主な目的としているため、切り捨てを回避するのに非常に優れている。他の型と同様、特定のビット数に基づいているため、オーバーフローが発生しやすいが、整数と比較すると、サポートできる値の範囲は広くなっている。さらに、オーバーフローが発生した場合、何も表示されないのではなく、正の無限大および負の無限大という特別な値が割り当てられる。
| 型 | ビット数 | 初期値 |
|---|---|---|
| float | 32 | float.nan |
| double | 64 | double.nan |
| real | 少なくとも64、場合によってはそれ以上 (例:80、ハードウェアのサポートに依存) | real.nan |
浮動小数点型のプロパティ
浮動小数点型には、他の型よりも多くのプロパティがある。
.stringofは型の名前だ。.sizeofは、バイト単位の型の長さだ。(ビット数を決定するには、この値に8(1バイトのビット数)を掛ける必要がある。)-
.maxは"最大"の略で、この型が持つことができる最大値だ。浮動小数点型には、.minプロパティは別途ない。.maxの負の値が、この型が持つことができる最小値だ。例えば、doubleの最小値は-double.maxだ。 -
.min_normalは、この型が通常の精度で表現できる最小の正の値である。(精度については後で説明する。) この型は、.min_normalよりも小さい値も表現できるが、それらの値は、この型の他の値ほど正確ではなく、一般的に計算速度も遅くなる。浮動小数点値が、負の.min_normalと正の.min_normal(0を除く)の間にある状態を、アンダーフローと呼ぶ。 -
.digは"digits"の略で、型の精度を表す桁数を指定する。 -
.infinityは、オーバーフローを表すために使用される特別な値だ。
浮動小数点型のその他のプロパティは、あまり一般的ではない。これらのプロパティはすべて、dlang.orgの"浮動小数点型のプロパティ"で確認できる。
浮動小数点型のプロパティとその関係は、次のような数直線上に表すことができる。
+ +─────────+─────────+ ... + ... +─────────+─────────+ + │ -max -1 │ 0 │ 1 max │ │ │ │ │ -infinity -min_normal min_normal infinity
2つの特別な無限大値を除き、上の線は縮尺である。min_normalと1の間で表現できる値の数は、1とmaxの間で表現できる値の数と同じだ。これは、min_normalと1の間の値の小数部の精度が非常に高いことを意味する。(負の側についても同様だ。)
.nan
これは浮動小数点変数のデフォルト値であることはすでに説明した。.nanは、意味のない浮動小数点式の結果としても現れることがある。例えば、次のプログラムの浮動小数点式はすべてdouble.nanとなる。
.nanは、初期化されていない値を示すというだけではない。計算を通じて伝播されるため、エラーを早期に、かつ容易に検出できるという点で有用だ。
浮動小数点値の指定
浮動小数点値は、123のような小数点のない整数値から構築することも、123.0のように小数点を使って直接作成することもできる。
浮動小数点値は、1.23e+4のように、特別な浮動小数点構文を使用して指定することもできる。この構文のe+部分は、"10の累乗"と読み替えることができる。この読み方によると、前の値は"1.23×10の4乗"となり、"1.23×104"と同じになる。これは1.23×10000と同じで、12300に等しくなる。
eの後の値が負の場合、5.67e-3のように、これは"10の累乗で割る"と読む。したがって、この例は"5.67を103で割った"となり、これは5.67/1000と同じで、0.00567に等しい。
3つの浮動小数点型のプロパティを出力する次のプログラムの出力では、浮動小数点形式が明らかだ。
私の環境でのプログラムの出力は次の通りだ。realはハードウェアに依存するため、異なる出力が得られる可能性がある:
| 型 | float |
|---|---|
| 精度 | 6 |
| 最小正規化値 | 1.17549e-38 |
| 最小値 | -3.40282e+38 |
| 最大値 | 3.40282e+38 |
| 型 | double |
|---|---|
| 精度 | 15 |
| 最小正規化値 | 2.22507e-308 |
| 最小値 | -1.79769e+308 |
| 最大値 | 1.79769e+308 |
| 型 | real |
|---|---|
| 精度 | 18 |
| 最小正規化値 | 3.3621e-4932 |
| 最小値 | -1.18973e+4932 |
| 最大値 | 1.18973e+4932 |
注釈: doubleおよびrealはfloatよりも精度が高いが、writelnはすべての浮動小数点値を6桁の精度で出力する。(精度については後で説明する。)
観察
前の章で覚えていると思うが、ulongの最大値は20桁で、18,446,744,073,709,551,616だ。この値は、最小の浮動小数点型と比較しても小さいように見える。floatは、1038の範囲までの値、例えば、340,282,000,000,000,000,000,000,000,000,000,000,000までの値を持つことができる。realの最大値は104932の範囲であり、4900桁以上の値になる。
別の例として、doubleが15桁の精度で表現できる最小値を見てみよう。
0.000...(ここにさらに300個のゼロがある)...0000222507385850720
オーバーフローは無視されない
非常に大きな値を取ることができるにもかかわらず、浮動小数点型もオーバーフローしやすい。この点では、オーバーフローは無視されないため、浮動小数点型は整数型よりも安全だ。正の側でオーバーフローした値は.infinityになり、負の側でオーバーフローした値は‑.infinityになる。これを確認するために、.maxの値を10%増やしよう。値はすでに最大値なので、10%増やすとオーバーフローする。
値がオーバーフローしてreal.infinityになると、半分に分割してもその値のままになる。
| その前 | 1.18973e+4932 |
|---|---|
| 10%を追加した | inf |
| 半分に分割された | inf |
精度
精度とは、日常生活の中でよく目にするが、あまり話題には上らない概念である。精度とは、値を指定する際に使用する桁数のことだ。例えば、100の3分の1は33だが、33は2桁なので、精度は2だ。値を33.33とより正確に指定すると、精度は4桁になる。
各浮動小数点型が持つビット数は、その最大値だけでなく、精度にも影響する。ビット数が多いほど、値の精度が高くなる。
除算では切り捨てはない
前の章で見たように、整数除算では結果の小数部を保持できない:
出力:
1
浮動小数点型にはこの切り捨ての問題はない。浮動小数点型は、小数部分を保持するように特別に設計されている。
出力:
1.5
小数部の精度は、型の精度によって異なる。realは精度が最も高く、floatは精度が最も低い。
使用する型の選択
特に理由がない限り、浮動小数点値にはdoubleを選択。floatは精度が低いが、他の型よりもサイズが小さいため、メモリが限られている場合に便利である。一方、一部のハードウェアでは、realの精度はdoubleよりも高いため、高精度の計算にはrealが適している。
すべての値を表現できない
日常生活では、特定の値を表現できないことがある。私たちが日常的に使用している 10進法では、小数点前の数字は1、10、100などを表し、小数点後の数字は10分の1、100分の1、1000分の1などを表す。
これらの値を組み合わせることで、あらゆる値を正確に表現することができる。例えば、0.23という値は2の10分の1と3の100分の1で構成されているため、正確に表現することができる。一方、1/3という値は、10進数では、いくら指定しても桁数が不足するため、正確に表現することができない。
浮動小数点型の場合も、状況は非常によく似ている。これらの型は、特定のビット数に基づいているため、すべての値を正確に表現することはできない。
コンピュータが使用する2進法との違いは、小数点前の桁は1、2、4などであり、小数点後の桁は0.5、0.25、0.125などであることである。これらの桁の正確な組み合わせの値のみが正確に表現できる。
コンピュータが使用する2進数では、10セントの0.1という値は正確に表現できない。この値は10進数では正確に表現できるが、2進数では4桁が0.0001100110011...と無限に繰り返される。(この値は10進数ではなく2進数で記述されていることに注意。) 使用する浮動小数点型の精度に応じて、あるレベルでは常に不正確になる。
次のプログラムは、この問題を示している。変数の値が、ループで1000回0.001ずつ増加している。驚くべきことに、結果は1にならない。
0.001を正確に表現できないため、その不正確さが各反復で結果に影響を与えるからだ:
異なる: 0.999991
注釈:上記の変数counterはループカウンタだ。この目的のために変数を明示的に定義することは推奨されない。代わりに、一般的なアプローチとしては、後で説明するforeachループを使う方法がある。
順序の無さ
整数で説明したのと同じ比較演算子は、浮動小数点型でも使用される。ただし、.nanは無効な浮動小数点値を表す特別な値であるため、.nanを他の値と比較することは意味がない。例えば、.nanと1のどちらが大きいかを比較することは意味がない。
そのため、浮動小数点値には別の比較概念、つまり順序性がないことが導入されている。順序性がないということは、少なくとも一方の値が.nanであることを意味する。
次の表は、すべての浮動小数点比較演算子を一覧表示している。これらはすべて二項演算子(2つのオペランドを取る)であり、left == rightと同じように使用される。falseとtrueを含む列は、比較演算の結果を示す。
最後の列は、オペランドの一方が.nanの場合に演算が意味を持つかどうかを示している。例えば、式1.2 < real.nanの結果はfalseだが、オペランドの一方がreal.nanであるため、この結果は意味を持たない。逆の比較real.nan < 1.2の結果も、falseとなる。lhsは左辺を表し、各演算子の左側にある式を示す。
| 演算子 | 意味 | lhsがより大きい場合 | lhsが小さい場合 | が両方とも等しい場合 | の少なくとも一方が .nanの場合 | .nanを含む意味のある |
|---|---|---|---|---|---|---|
| == | は等しい | false | false | true | false | はい |
| != | と等しくない | true | true | false | true | はい |
| > | より大きい | true | false | false | false | いいえ |
| >= | 以上 | true | false | true | false | いいえ |
| < | より小さい | false | true | false | false | ない |
| <= | 以下 | false | true | true | false | ない |
.nanと一緒に使用すると意味があるが、==演算子は、.nan値と一緒に使用すると、常にfalseを返す。これは、両方の値が.nanの場合でも同様である。
double.nanが自身と等しいと予想されるが、比較の結果はfalseとなる:
等しくない
isNaN() .nanの等価比較
前述のように、==演算子を使用して、浮動小数点変数の値が.nanであるかどうかを判断することはできない。
std.mathモジュールにあるisNaN()関数は、値が.nanであるかどうかを判断するためのものだ。
同様に、値が.nanではないかどうかを判断するには、!isNaN()を使用する必要がある。そうしないと、!=演算子は常にtrueを返すからだ。
演習
- 上記のプログラムで、0.001を1000回加算する部分で、
floatの代わりにdouble(またはreal)を使用しよう:この演習は、浮動小数点数の等価比較がどれほど誤解を招きやすいかを示している。
- 前の章の計算機を、浮動小数点型に対応するように変更。変更後の計算機は、より正確に動作するはずだ。計算機を試す際には、1000、1.23、1.23e4など、さまざまな形式の浮動小数点値を入力してみよう。
- 入力から5つの浮動小数点値を読み込むプログラムを書いてみよう。このプログラムでは、各値を2回ずつ出力してから、各値の5分の1を出力するようにしよう。
この演習は、次の章で説明する配列の概念の準備である。これまで学んだ内容でこのプログラムを書くと、配列をより理解しやすく、その重要性をより深く理解できるだろう。