tmori3y2のブログ

主にWindowsのプログラムなど

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

https://ja.wikipedia.org/wiki/%E5%8D%98%E7%B2%BE%E5%BA%A6%E6%B5%AE%E5%8B%95%E5%B0%8F%E6%95%B0%E7%82%B9%E6%95%B0

 log10(2^{24}) \fallingdotseq 7.225

ということで、よく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進数の桁数の関係は以下で表せる

     2^{10} \fallingdotseq 10^{3}

  • フルビットの小数部を表すのに以下の公式が使える

     \displaystyle
\begin{eqnarray}
\sum_{n=0}^{N} 2^{n}
\end{eqnarray} = 2^{N + 1} - 1

ので、覚えておくと良い。

正規化数の絶対値の最大値

# 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のとき、以下の式で表現できる小数部は、誤差のない小数となる

     \displaystyle
\begin{eqnarray}
\sum_{n=-1}^{-N} a_n \times 2^{n}
\end{eqnarray}
(a_n = 0, 1)

  • 小数部の2進数表現のビット数が、小数部の2進数表現の有効ビット数の範囲を超えたとき、下位の不足ビットが丸められた近似値となる

    • 誤差の出ない小数でビット数が足りないとき

    • 2進数表現で循環小数、もしくは、無限小数となるとき

      • 例) 10進数の0.01は2進数表現で循環小数になるので、ビット数が有限の場合には丸め誤差が発生する

        0.01 (10) = 0.00(00001010001111010111) (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
  •  2^{10} \fallingdotseq 10^{3}なので、小数点以下が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でも公開中。