丸め誤差

突然ですが,C言語で

#define x1 0.5;
#define x2 0.1;

float sum1 = 0;
float sum2 = 0;

int i;

for(i = 0;i < 2;i++){sum1+=x1;}
for(i = 0;i < 10;i++){sum2+=x2;}

printf("%.16f\t%.16f\n" , sum1 , sum2);

を実行した結果を考えてみてほしい.

x1は0.5固定で,sum1に2回足される.つまり,sum1=0.5+0.5=1.0

x2は0.1固定で,sum2に10回足される.つまり,sum2=0.1+0.1+0.1+0.1+0.1+0.1+0.1+0.1+0.1+0.1=1.0

 

両方同じ結果だと思いますよね.それは,数学の話.

実行結果は次のようになります.

1.0000000000000000 1.0000001192092896

これは,コンピュータが本質的に孕んでいる問題で,C言語であろうと他の言語であろうと起こります.正確には,IEEE754という規格を採択しているとこういうことが起こります.

コンピュータで扱う実数は10進数ではなく2進数で,例えば0.1は

0.1_{(10)}=0.0001100110011001100\cdots _{(2)}

と無限に続く循環小数です.コンピュータのメモリは有限なので,どこかで打ち切らなければなりません.そこで,メモリになるだけ多くの小数を格納することを考えます.

さっきの例だと,最初の4つ続いている0はこれだけで4bitsも消費してしまい,bitの無駄です.指数は2^{-4}だから,-4_{(10)}=1100_{(2)}です(補数表現).この例では,減らせるbitはありませんでしたが,2^{-10}とかだと0が10個続く(10bits)のが,-10_{(10)}=1010_{(2)}なのでやっぱり4bitになり,6bitの短縮になりますね.つまり,2の指数を考えれば大幅にbitを減らせます.

すると,1.10011001100110011001100_{(10)}\cdots \times 2^{-4}となります.以降小数部分だけ考えます.

上の考え方を使えば,小数点より左は常に1なので,これにbitを割く必要はありません.これでもう1bit削れました.

さて,ここからがいよいよ打ち切りの問題.どこまで残すか.

IEEE754では単精度浮動小数点数(single, C言語ではfloat型)は小数部23bitsと定められていますので,次のように打ち切られます(|で打ち切られる)

0.1_{(10)} \to 1.\overbrace{10011001100110011001100}^{23{\rm bits}}|11\cdots _{(2)} \times 2^{ - 4}

そして,この入りきらなかった数字”11″は「丸め」られます.といっても磨くわけではありません.

IEEE754では,5種類の丸め方が定義されているのですが,最も用いられているのが(デフォルトで使われるのが),「最近接偶数丸め」です.すごい名前ですね.

「最近接偶数丸め」でググったらヒットするページのほとんどが10進数の四捨五入の話をしていて,コンピュータ内部の2進数処理とは異なっているので,本質を欠いている!!と思ったのが,このエントリを書こうと思った理由です.

「最近接偶数丸め」とは,基本的には零捨一入でちょうど中間の値の時には丸めた結果が偶数になる方をとる,という説明が2進数ではよくされます,が,よく分かりませんね.具体的には,

\cdots 00|00 \to 00

\cdots 00|01\to 00

\cdots {\bf 00|10 \to 00}

\cdots 00|11 \to 01

\cdots 01|00 \to 01

\cdots 01|01 \to 01

\cdots {\bf 01|10 \to 10}

\cdots 01|11 \to 10

という具合で丸めます.気をつけて欲しいのは,太字のところで,これが唯一,零捨一入と異なるところです.最近接偶数丸めでは,最下位ビットから2つ下のビットまで読み,それが”10″のときは,丸めた時の最下位ビットが0になるように丸めます.上の”10″が入りきらなかった場合,強制的に繰り上げるのが零捨一入です.

零捨一入では,最下位ビットのその1つ下の数しか見ないで繰り上げ,繰り下げを判断するのですが,これだと誤差が大きくなり,丸める前の和と丸めた後の和で誤差が発生します.(この誤差をバイアスと言います)

これは話題から逸れるのであまり詳しくは書きませんが,零捨一入では,例えば

1|00 \to 1

1|01 \to 1

0|10 \to 1

0|11 \to 1

ですが,これらの丸める前の和は11|10で,丸めた後の和は100です.

一方,最近接偶数丸めでは

1|00 \to 1

1|01 \to 1

0|11 \to 1

ですが,これらの丸める前の和は11|00で,丸めた後の和も11です.

 

冒頭のプログラムにおいて,0.1を10回足した方がうまくいかなかったのは,0.1を単精度浮動小数点数で表した時に丸め誤差が発生し,それが蓄積されていった,ということです.逆に,0.5を2回足した方がうまくいったのは,0.5_{(10)}=0.1_{(2)}なので単精度浮動小数点数で表しても正確に表現でき,丸める必要がなかったためだと言えます.

さて,0.1がうまくいかなかったのは丸め誤差の蓄積だということは説明しましたが,倍精度浮動小数点数(double型)ではどうでしょうか.

実行結果は,

1.0000000000000000 0.9999999999999999

となります.

double型というのは,小数部を52bits格納できます.大事なことは,23bitsみたいに奇数でなく,偶数ビット格納できるということです.これにより,0.1_{(10)} = 0.000\dot{1}10\dot{0}という4bitを周期とする循環小数がきちんと格納されます.

広告

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中