カニゲーム攻略日記ブログ

beatmaniaIIDXやハースストーンなどのゲーム攻略日記。主にまったり勢。2016年にIIDX皆伝になった

プログラムはなぜ動くのか  第3章 コンピュータが小数点数の計算を間違える理由

プログラムはなぜ動くのか  第3章 コンピュータが小数点数の計算を間違える理由

「小数点数はすべてのプログラマが身に付けておかねばならない基礎知識」
と本に書いてあるが、
別に知らなくてもいいと思う
小数点数の計算が必須の方が知ってればいいと思う
とりあえず、やってみるさ

小数点数とは?

小数点数とは、整数部分と小数部分を持つ数値のことです。整数部分は小数点の左側に位置し、小数部分は小数点の右側に位置します。例えば、「3.14」は小数点数で、整数部分が「3」、小数部分が「14」です。小数点数は、現実世界の連続的な量を表現するために広く使われています。

0.1を100回足しても10にならない

コンピュータの話
以下の計算ルールでは10にならないけど、ルールを変えれば10にすることもできる

小数点を2進数で表す

こんなルールなの

2^-1 = 1/2 = 0.5  
2^-2 = 1/2/2 = 1/4 = 0.25
・・・

例:1.1111

1.1111
= 1*2^0 + 1*2^-1 + 1*2^-2 + 1*2^-3 + 1*2^-4
= 1*1 + 1*0.5 + 1*0.25 + 1*0.125 + 1*0.0625
= 1.9375

0.1を2進数で表すと無限の世界へ

0.1
=0.00001100110011001100・・・(1100の繰り返し)

0.1を100回足してみる

for(i = 1; i <= 100; i++){
  sum += 0.1;
}

結果(本の場合)

10.000002

計算イメージを考えていたがよく分からなくなってきた

0.00001100110011001100 + 0.00001100110011001100 + (・・・合計100回)
= ?

chatGPTさんに聞く

10進数を2進数に置き換えると、浮動小数点数の0.1を100回足す例を考えると、誤差が発生することが理解しやすくなります。

10進数の0.1は、2進数では無限循環小数になります。具体的には、0.1 (10進数) = 0.00011001100110011... (2進数) となります。コンピュータでは、この無限循環小数を正確に表現できないため、誤差が発生します。

C言語で0.1 (10進数)を100回足す例を再度見てみましょう。

#include <stdio.h>

int main() {
    float decimal = 0.1;
    float sum = 0;
    int i;

    for (i = 0; i < 100; i++) {
        sum += decimal;
    }

    printf("Sum: %.8f\n", sum); // Sum: 9.99999905

    return 0;
}

ここでは、出力の精度を8桁に設定しています。この例で0.1を100回足すと、9.99999905という結果が得られます。これは、0.1の2進数表現が無限循環小数であり、浮動小数点数の精度に制限があるためです。

このような誤差が問題となる場合は、適切な数値表現やアルゴリズムを検討する必要があります。例えば、固定小数点数や適切なスケーリングを使用することで、誤差を軽減することができます。

2進数の0.00011001100110011同士の足し算を10回繰り返す

以下がその結果です。

  0.00011001100110011 (1回目)
+ 0.00011001100110011
---------------------
  0.00110011001100110

  0.00110011001100110 (2回目)
+ 0.00011001100110011
---------------------
  0.01000100110101001

  0.01000100110101001 (3回目)
+ 0.00011001100110011
---------------------
  0.01011110011011100

  0.01011110011011100 (4回目)
+ 0.00011001100110011
---------------------
  0.01110000000001111

  0.01110000000001111 (5回目)
+ 0.00011001100110011
---------------------
  0.10001001100111010

  0.10001001100111010 (6回目)
+ 0.00011001100110011
---------------------
  0.10010011001101101

  0.10010011001101101 (7回目)
+ 0.00011001100110011
---------------------
  0.10100000110100100

  0.10100000110100100 (8回目)
+ 0.00011001100110011
---------------------
  0.10111010100011101

  0.10111010100011101 (9回目)
+ 0.00011001100110011
---------------------
  0.11010101011010000

  0.11010101011010000 (10回目)
+ 0.00011001100110011
---------------------
  0.11101110000100011

10回繰り返した結果は 0.11101110000100011 です。

10進数に変換する

2進数の 0.11101110000100011 を10進数に変換すると以下のようになります。

0.11101110000100011(2進数) =  
 1 * 2^(-1) + 1 * 2^(-2) + 1 * 2^(-3) + 0 * 2^(-4) + 1 * 2^(-5) + 1 * 2^(-6) + 1 * 2^(-7) + 0 * 2^(-8) + 0 * 2^(-9) + 0 * 2^(-10) + 1 * 2^(-11) + 0 * 2^(-12) + 0 * 2^(-13) + 0 * 2^(-14) + 1 * 2^(-15) + 1 * 2^(-16)

= 0.5 + 0.25 + 0.125 + 0 + 0.03125 + 0.015625 + 0.0078125 + 0 + 0 + 0 + 0.00048828125 + 0 + 0 + 0 + 0.000030517578125 + 0.0000152587890625

= 0.929931640625(10進数)

2進数の 0.11101110000100011 は、10進数で 0.929931640625 になります。

コンピュータの浮動小数点数とは

いろいろ省略すると以下のようなルール

例:0.75 (32bit)

0-01111110-10000000000000000000000

0:符号部
  0の場合は+ 
  1の場合は-  

01111110:指数部  
  イクセス表現なので126-127=-1

10000000000000000000000:仮数部  
  1.1を意味する

2進数で1.1 * 2^-1を表す  

10進数で1.5 * 2^-1  
= 1.5 * 0.5  
= 0.75 を表す

イクセス表現

イクセス表現(Excess notation)は、符号付き整数を符号なし整数として表現する方法で、オフセットバイナリ表現とも呼ばれます。特定のオフセット(通常は2n-1、nはビット数)を整数に加算して、負の値を含む数値範囲を0から始まる符号なし整数として扱います。

例えば、4ビットのイクセス表現では、オフセットは24-1 = 8です。この場合、-8は0000、-1は0111、0は1000、7は1111と表現されます。イクセス表現では、符号付き整数がオフセット分だけシフトされて、符号なし整数として扱われます。

仮数

仮数部とは、浮動小数点数を表現する際に使用される、数値の実際の桁を示す部分です。通常、1を省略した正規化された2進数の形式で表現されます。例えば、10進数の数値「0.75」は2進数で「0.11」、正規化された形式では指数部分を分離して「1.1 x 2-1」となり、仮数部は「1.1」になります。

余談

小数点数の計算したい時に振り返ればいいや
それまでは記憶の片隅へ✨