Single型について
だんだん、更新間隔が長くなってます。反省。
今回も、新人教育ネタです。
若手の方の2進数脳のトレーニングになれば幸いかと。
前置き
System.Decimal (decimal)は、10進数の浮動小数点型なので、取っ付きやすいが、2進数の浮動小数点型は、中々そうはいかない。
私の会社は、メカ屋とその他という括りで、ソフトウェア工学を専攻していない学生さんが、ソフト屋に配属されてくるので、一般的な2進数浮動小数点型の解説では不十分じゃないかと常々思っている。
特に、2進数の世界の話なのに、10進数での有効桁数が前面に出てきてしまい、本来の2進数での有効桁数 (有効ビット数)での理解がおろそかになってはしないかと危惧しているので、今回の記事では、その辺を何とか視覚化できないかと思っている。
System.Single (float)
System.Single(float)は、IEEE 754に準拠した単精度浮動小数型で、2進数表記で24桁(24bit)、もしくは、23桁(23bit)の小数。
https://msdn.microsoft.com/ja-jp/library/system.single%28v=vs.110%29.aspx
ということで、よく10進数7桁の精度と一括りにされるが、実際には条件によっては8桁でも正確なケースがあるのに、C#の実装でもそれが考慮されていないケースがあるので、注意が必要である。
今回は、2進数表現、および、有効桁数を8桁とした場合の10進数表現で、どうなるかを簡単なテストプログラムで示してみた。
https://dotnetfiddle.net/tt7mco
通常なら可能な限りDoubleを使うが、ビット数が違うだけで本質的には同じで、桁数が多い分、視覚化したときに見にくいので、教育目的ならSingleの方が良いだろうという意味で、DoubleではなくSingleを選択した。
ビットレイアウト
32bit単精度浮動小数点型のビットレイアウトは以下のようになっている。
Bit 0-22: 有効桁 or 仮数部 (Significant Bits)
Bit 23-30: 指数部 (Exponent Bits)
Bit 31: 符号 (Sign)
テストコードの実行結果でも、極力このレイアウトが分かるように、出力結果を調整しているので、見てほしい。
Zero
指数部 (Exponent Bits)が0、かつ、有効桁 or 仮数部 (Significant Bits)が0のとき、+、および、-のZeroとなる。
# Zero Zero : 0_00000000_00000000000000000000000 = (-1)^0 * 2^(-126) * 0.00000000000000000000000 = 0.00000000E+000 -Zero : 1_00000000_00000000000000000000000 = (-1)^1 * 2^(-126) * 0.00000000000000000000000 = 0.00000000E+000 -Zero (0x80000000) : 1_00000000_00000000000000000000000 = (-1)^1 * 2^(-126) * 0.00000000000000000000000 = 0.00000000E+000
整数型と同じく、ゼロは「ゼロ」である。
正規化数 (Normalized Number)
指数部 (Exponent Bits)が0x01~0xFEのとき、
(-1)Sign * 2Exponent - 127 * 1.Significant (2)
の正規化された数となる。
正規化数は、固定ビットも含めて、2進数24桁 (24bit)の精度を持つ。
感覚的には2進数と10進数の桁数の関係は以下で表せる
フルビットの小数部を表すのに以下の公式が使える
ので、覚えておくと良い。
正規化数の絶対値の最大値
# Normalized number furthest from zero MinValue : 1_11111110_11111111111111111111111 = (-1)^1 * 2^( 127) * 1.11111111111111111111111 = -3.40282347E+038 MaxValue : 0_11111110_11111111111111111111111 = (-1)^0 * 2^( 127) * 1.11111111111111111111111 = 3.40282347E+038 2^(128) - 2^(104) : 0_11111110_11111111111111111111111 = (-1)^0 * 2^( 127) * 1.11111111111111111111111 = 3.40282347E+038
正規化数の絶対値の最大値は、
2127 * 1.11111111111111111111111 (2) = 2128 - 2104 = Single.MaxValue
で、
Single.MinValue = - Single.MaxValue
である。
正規化数の絶対値の最小値
# Normalized number closest to zero 2^(-126) : 0_00000001_00000000000000000000000 = (-1)^0 * 2^(-126) * 1.00000000000000000000000 = 1.17549435E-038
正規化数の絶対値の最小値は、
2-126 * 1.00000000000000000000000 (2) = 2-126
である。
正規化数よりも絶対値が小さな値として、非正規化数があるので、2-126が0でない絶対値の最小値でない点に注意してほしい。
正規化数の整数部
一般に整数型を浮動小数点型にキャストしたときには、以下のことが言える。
整数型の2進数表現のビット数が、浮動小数点型の正規化数の有効桁数(ビット数)の範囲内では、キャストをしても誤差のない整数となる
整数型の2進数表現のビット数が、浮動小数点型の正規化数の有効桁数(ビット数)の範囲を超えたとき、キャストにより下位の不足ビットが丸められた近似値となる
32bit単精度浮動小数型の正規化数の整数については、正規化数の定義を眺めてみると、
32bit単精度浮動小数型で正確に表現できる連続する整数の正規化数は、絶対値の1から224 = 16777216まで
224を超えると、最上位が2Nの桁のとき、2N - 23までが有効桁となり、間隔が2N - 23の離散値となる
ということが分かる。
# Normalized number near to 2^(24) 2^(24) - 3 : 0_10010110_11111111111111111111101 = (-1)^0 * 2^( 23) * 1.11111111111111111111101 = 16777213 2^(24) - 2 : 0_10010110_11111111111111111111110 = (-1)^0 * 2^( 23) * 1.11111111111111111111110 = 16777214 2^(24) - 1 : 0_10010110_11111111111111111111111 = (-1)^0 * 2^( 23) * 1.11111111111111111111111 = 16777215 2^(24) : 0_10010111_00000000000000000000000 = (-1)^0 * 2^( 24) * 1.00000000000000000000000 = 16777216 2^(24) + 1 : 0_10010111_00000000000000000000000 = (-1)^0 * 2^( 24) * 1.00000000000000000000000 = 16777216 2^(24) + 2 : 0_10010111_00000000000000000000001 = (-1)^0 * 2^( 24) * 1.00000000000000000000001 = 16777218 2^(24) + 3 : 0_10010111_00000000000000000000010 = (-1)^0 * 2^( 24) * 1.00000000000000000000010 = 16777220 2^(24) + 4 : 0_10010111_00000000000000000000010 = (-1)^0 * 2^( 24) * 1.00000000000000000000010 = 16777220
224近傍を見たとき、
224未満では、223から20まで有効な桁なので、丸め処理は行われずに正確な整数となる
224以上では、224から21まで有効な桁なので、20が銀行丸め処理される
丸められるビット20が0ならそのまま
- 16777216/16777218/16777220はそのまま
丸められるビット20が中間値の1、かつ、21が0なら切り下げ
- 16777217は16777216に切り下げられる
丸められるビット20が中間値の1、かつ、21が1なら切り上げ
- 16777219は16777220に切り上げられる
# Normalized number near to 2^(25) 2^(25) - 4 : 0_10010111_11111111111111111111110 = (-1)^0 * 2^( 24) * 1.11111111111111111111110 = 33554428 2^(25) - 3 : 0_10010111_11111111111111111111110 = (-1)^0 * 2^( 24) * 1.11111111111111111111110 = 33554428 2^(25) - 2 : 0_10010111_11111111111111111111111 = (-1)^0 * 2^( 24) * 1.11111111111111111111111 = 33554430 2^(25) - 1 : 0_10011000_00000000000000000000000 = (-1)^0 * 2^( 25) * 1.00000000000000000000000 = 33554432 2^(25) : 0_10011000_00000000000000000000000 = (-1)^0 * 2^( 25) * 1.00000000000000000000000 = 33554432 2^(25) + 1 : 0_10011000_00000000000000000000000 = (-1)^0 * 2^( 25) * 1.00000000000000000000000 = 33554432 2^(25) + 2 : 0_10011000_00000000000000000000000 = (-1)^0 * 2^( 25) * 1.00000000000000000000000 = 33554432 2^(25) + 3 : 0_10011000_00000000000000000000001 = (-1)^0 * 2^( 25) * 1.00000000000000000000001 = 33554436 2^(25) + 4 : 0_10011000_00000000000000000000001 = (-1)^0 * 2^( 25) * 1.00000000000000000000001 = 33554436 2^(25) + 5 : 0_10011000_00000000000000000000001 = (-1)^0 * 2^( 25) * 1.00000000000000000000001 = 33554436 2^(25) + 6 : 0_10011000_00000000000000000000010 = (-1)^0 * 2^( 25) * 1.00000000000000000000010 = 33554440 2^(25) + 7 : 0_10011000_00000000000000000000010 = (-1)^0 * 2^( 25) * 1.00000000000000000000010 = 33554440 2^(25) + 8 : 0_10011000_00000000000000000000010 = (-1)^0 * 2^( 25) * 1.00000000000000000000010 = 33554440
225近傍を見たとき、
225未満では、224から21まで有効な桁なので、20が銀行丸め処理される
丸められるビット20が0ならそのまま
- 33554428/33554430/33554432はそのまま
丸められるビット20が中間値の1、かつ、21が0なら切り下げ
- 33554429は33554428に切り下げられる
丸められるビット20が中間値の1、かつ、21が1なら切り上げ
- 33554431は33554432に切り上げられる
225以上では、225から22まで有効な桁なので、21から20までが銀行丸め処理される
丸められるビット21から20が00ならそのまま
- 33554432/33554436/33554440はそのまま
丸められるビット21から20が01ならは切り下げ
33554433は33554432に切り下げられる
33554437は33554436に切り下げられる
丸められるビット21から20が11ならは切り上げ
33554435は33554436に切り上げられる
33554439は33554440に切り上げられる
丸められるビット21から20が中間値の10、かつ、22が0なら切り下げ
- 33554434は33554432に切り下げられる
丸められるビット21から20が中間値の10、かつ、22が1なら切り上げ
- 33554438は33554440に切り上げられる
初心者の勘違いとしては、銀行丸めが10進数で行われると思われがちだが、間違いである。
今回のようにテストプログラムを書いてみればわかるように、丸めは2進数で行われていることに注意が必要である。
Decimal型への変換の問題
https://msdn.microsoft.com/ja-jp/library/he38a8ca(v=vs.110).aspx
# Decimal convert issue of normalized number near to 2^(24) 2^(24) - 3 : To Decimal: 16777210, To Double: 16777213, To Int32: 16777213 2^(24) - 2 : To Decimal: 16777210, To Double: 16777214, To Int32: 16777214 2^(24) - 1 : To Decimal: 16777220, To Double: 16777215, To Int32: 16777215 2^(24) : To Decimal: 16777220, To Double: 16777216, To Int32: 16777216
Convert.ToDecimal(Single)は、有効桁数7桁で丸めてDecimalを返すメソッドなので、107から224までの整数は正確に変換できない
- SingleからDecimalへのキャストも同じ結果になる
Convert.ToInt32(Single)や、Convert.ToDouble(Single)、Convert.ToSingle(Decimal)は、この範囲の整数を正確に変換できる
Convert.ToDecimal(Single)の実装はリファレンスに書かれた仕様通りだが、その仕様が妥当かは甚だ疑問
(2016/12/07 追記) CComVariant/_variant_tでもChangeType(VT_DECIMAL)で同じ挙動になったので、Microsoftのプラットフォームとしては一貫しているともいえる。調べていないが、規格があるのかもしれない
小数部は元々近似値なので、10進数で8桁目に相当する小数部が丸められる分には問題がないと思われる
対処策としては、DoubleにキャストしてからDecimalに変換する
なお、Convert.ToDecimal(Double)も有効桁数15で丸められているので、天文学的数値を扱わない場合はそれほど影響はないものの、同様の問題がある。
正規化数の小数部
一般に浮動小数点型の小数部について、以下のことが言える。
小数部の2進数表現の有効ビット数は、浮動小数点型の正規化数の有効桁数(ビット数)から、整数部のビット数を差し引いたものになる
小数部の2進数表現の有効ビット数がNのとき、以下の式で表現できる小数部は、誤差のない小数となる
小数部の2進数表現のビット数が、小数部の2進数表現の有効ビット数の範囲を超えたとき、下位の不足ビットが丸められた近似値となる
10進数小数の2進数表現での誤差の例
# Normalized number less than 1.0 0.001 : 0_01110101_00000110001001001101111 = (-1)^0 * 2^( -10) * 1.00000110001001001101111 = 0.00100000005 0.01 : 0_01111000_01000111101011100001010 = (-1)^0 * 2^( -7) * 1.01000111101011100001010 = 0.00999999978 0.1 : 0_01111011_10011001100110011001101 = (-1)^0 * 2^( -4) * 1.10011001100110011001101 = 0.100000001 0.2 : 0_01111100_10011001100110011001101 = (-1)^0 * 2^( -3) * 1.10011001100110011001101 = 0.200000003 0.3 : 0_01111101_00110011001100110011010 = (-1)^0 * 2^( -2) * 1.00110011001100110011010 = 0.300000012 0.4 : 0_01111101_10011001100110011001101 = (-1)^0 * 2^( -2) * 1.10011001100110011001101 = 0.400000006 0.5 : 0_01111110_00000000000000000000000 = (-1)^0 * 2^( -1) * 1.00000000000000000000000 = 0.5 0.6 : 0_01111110_00110011001100110011010 = (-1)^0 * 2^( -1) * 1.00110011001100110011010 = 0.600000024 0.7 : 0_01111110_01100110011001100110011 = (-1)^0 * 2^( -1) * 1.01100110011001100110011 = 0.699999988 0.8 : 0_01111110_10011001100110011001101 = (-1)^0 * 2^( -1) * 1.10011001100110011001101 = 0.800000012 0.9 : 0_01111110_11001100110011001100110 = (-1)^0 * 2^( -1) * 1.11001100110011001100110 = 0.899999976
標準書式での32bit単精度浮動小数型の丸めは、デフォルトで10進数7桁
2進数表記で誤差があることが分かりにくい
10進数で誤差が見えない様に意図的行われているもの
一方で、多くの初心者が2進数表現が近似値であることを忘れる/気づかない要因となっている
有効数字を8桁まで拡大すると、端数があるかないかを判断できる
10進数小数リテラルを代入した値の誤差の問題とは別に、誤差のある小数同士の計算結果の誤差は累積する
- 10進数の計算では同じ結果になる2つの異なる計算が、2進数では異なる計算結果になることも多い
整数部を含む小数の誤差の拡大例
さて、32bit単精度浮動小数型の正規化数の小数部については、正規化数の定義を眺めてみると、
1未満の小数の小数点以下を表現するための小数部のビット数は24
1から224までの範囲の小数の小数点以下を表現するための小数部のビット数は、 24 - <整数部のビット数>
となっている。
このため、殆どの小数は近似値となること、小数部が同じでも、整数部のビット数が増えると、小数部の近似精度は悪化することが分かる。
# Normalized number fraction part loss 0.001 : 0_01110101_00000110001001001101111 = 0.000000000100000110001001001101111 = 0.00100000005 1.001 : 0_01111111_00000000010000011000101 = 1.00000000010000011000101 = 1.00100005 10.001 : 0_10000010_01000000000010000011001 = 1010.00000000010000011001 = 10.0010004 100.001 : 0_10000101_10010000000000010000011 = 1100100.00000000010000011 = 100.000999 1000.001 : 0_10001000_11110100000000000010000 = 1111101000.00000000010000 = 1000.00098 10000.001 : 0_10001100_00111000100000000000001 = 10011100010000.0000000001 = 10000.001 100000.001 : 0_10001111_10000110101000000000000 = 11000011010100000.0000000 = 100000 1000000.001 : 0_10010010_11101000010010000000000 = 11110100001001000000.0000 = 1000000 10000000.001 : 0_10010110_00110001001011010000000 = 100110001001011010000000 = 10000000
なので、小数点以下が10進数3桁なら、24 * 1000 = 16000ぐらいまでが精度上の上限
ただし、Singleのデータに対して、小数点以下4桁以下を標準の銀行丸め以外で丸めたり、Singleで小数点以下4桁以下を入力したらNGとか判定しようとすると、更に10進数で一桁足りなくなるので、整数部が2000でも精度的に怪しくなる
この点について、浮動小数点型の話であまり説明されていないことが多いが、プログラムへの入力値としての各種パラメータでは小数点以下の桁数固定で扱うケースが殆どであることを考えると、整数部のビット数によって小数点以下の近似精度がどの程度影響を受けるのかは、把握しておいてほしいところである。
また、倍精度浮動小数点型でもビット数が異なるだけの話なので、今回のように人間の頭で想像力の働く桁数の単精度浮動小数点型で、定性的なとらえ方をしておく。
勿論、結論を言えば、
倍精度浮動小数点型の方が精度的に余裕があるので、そちらを使用すべき
更に言うなら、特定分野のプログラムでもない限り、Decimal型の方が正確な10進数を扱えるので、そちらを使用すべき
である。
ただし、過去の遺産や、組み込みシステムを含めた異種間通信等の仕様上の制約で、単精度浮動小数点型を選択してしまう場合もあるので、知識として必要である。
一番良いのは、入力チェックや丸め処理が必要な場合は、UIの処理はDecimal型で扱い、モデル層で浮動小数点型へ変換することだろう。
非正規化数 (Denormalized Number)
指数部 (Exponent Bits)が0x00、かつ、有効桁 or 仮数部 (Significant Bits)が0でないとき、
(-1)Sign * 2- 126 * 0.Significant (2)
の非正規化数となる。
非正規化数は、2進数1〜23桁 (1〜23bit)の精度を持つ。
Epsilon = 2^(-149) : 0_00000000_00000000000000000000001 = (-1)^0 * 2^(-126) * 0.00000000000000000000001 = 1.40129846E-045 2^(-127) : 0_00000000_10000000000000000000000 = (-1)^0 * 2^(-126) * 0.10000000000000000000000 = 5.87747175E-039 2^(-126) - 2^(-149) : 0_00000000_11111111111111111111111 = (-1)^0 * 2^(-126) * 0.11111111111111111111111 = 1.17549421E-038
非正規化数の絶対値の最小値は、
2-126 * 0.00000000000000000000001 (2) = 2-149 = Single.Epsilon
で、非正規化数の絶対値の最大値は、
2-126 * 0.11111111111111111111111 (2) = 2-126 - 2-149
である。
注意が必要なのは、Single.Epsilonは計算機イプシロンと呼ばれているものではないということだ。
# Machine Epsilon 2^(0) + 2^(-23) : 0_01111111_00000000000000000000001 = (-1)^0 * 2^( 0) * 1.00000000000000000000001 = 1 2^(0) : 0_01111111_00000000000000000000000 = (-1)^0 * 2^( 0) * 1.00000000000000000000000 = 1 2^(-23) : 0_01101000_00000000000000000000000 = (-1)^0 * 2^( -23) * 1.00000000000000000000000 = 1.1920929E-07
計算機イプシロンは、1より大きい最小の数と1の差として定義されている。
1より大きい最小の数は、正規化数1の有効桁 or 仮数部 (Significant Bits)の最下位ビットを1にした
20 * 1.00000000000000000000001 (2) = 20 + 2-23
であるので、計算機イプシロンは、そこから1を引いた2-23であることが分かる。
Ininity
指数部 (Exponent Bits)が0xFF、かつ、有効桁 or 仮数部 (Significant Bits)が0のとき、+、および、-のInfinityとなる。
-Infinity : 1_11111111_00000000000000000000000 = -Infinity +Infinity : 0_11111111_00000000000000000000000 = Infinity 1.0 / 0.0 : 0_11111111_00000000000000000000000 = Infinity
0で除算は通常例外が出るが、uncheckedで演算するとInfinityになることが分かる他、Single.IsInfinity()で判定することも可能である。
NaN
float型は、32bitの2進数で定義されるが、すべてのビット表現が、数としてみなされるわけではない。
数としてみなされないものを、非数 (Not a Number)と呼び、NaNと表記する。
NaN : 1_11111111_10000000000000000000000 = NaN NaN (0xFFFFFFFF) : 1_11111111_11111111111111111111111 = NaN NaN (0x7FFFFFFF) : 0_11111111_11111111111111111111111 = NaN
Single.NaNは、
Single.NaN = 1_11111111_10000000000000000000000 (2)
と定義されているが、指数部 (Exponent Bits)が0xFF、かつ、有効桁 or 仮数部 (Significant Bits)が0以外のとき、すべてNaNとなり、Single.IsNaN()はtrueとなる。
16進表記
浮動小数点型も、デバッガでメモリダンプしたり、WireShirkなどのプロトコルアナライザでネットワークパケットをキャプチャしてみたときは、16進数表記のバイトシーケンスで表示される。
# Hex String 0.00000000 : 00-00-00-00 0.10000000 : CD-CC-CC-3D 0.50000000 : 00-00-00-3F 1.00000000 : 00-00-80-3F 2.00000000 : 00-00-00-40 3.00000000 : 00-00-40-40 4.00000000 : 00-00-80-40
Single[]を含むデータをデバッグするときは、これらの値をダミーデータとして流してダンプすると、通信等のデバッグに役立つ。
今回、テストプログラムでは、BitConverter.GetBytes()を使用しているので、x86アーキテクチャのリトルエンディアンになっているが、組み込みシステムや通信系のプロトコルでは、ビッグエンディアンが採用されている場合もある。
例えば、0.0や0.5、2.0、3.0を並べてしまうと、エンディアンの判定やオフセット位置の判定が難しいが、0.1や1.0、4.0を選べば判定しやすいので、テストデータを上手く選ぶと、エンディアンや構造体のレイアウトが正しいかのチェックも出来る。、
テストコードは以下のGistでも公開中。