浮動小数点数と固定小数点数と誤差

[java]
public class Main {
	public static void main(String args[]){
		double a = 1.1;
		double b = 2.2;
		double result;

		result = a+b;
		System.out.println("a + b = " + result);

	}
}
[/java]

これを実行すると答えは3.3000000000000003

理由は丸め誤差。

コンピュータ内部では数値を2進で扱ってるので、0.1だとか0.2などの2進に変換すると循環小数になるものは正確に表現できない。

例えば0.2を2進にすると0.0011001100110011…となるけれど、いつかはこれを打ち切って端数処理せにゃならない。

仮に0.00110011で打ち切ったとすると1/8+1/16+1/128+1/256=0.19921875

その1個したの桁を切り上げるのか切り下げるのかでまた結果は変わる訳で、とにかく循環小数は正確に表現できない。

話はちょっと変わって、この小数だけど大抵は浮動小数点表現。つまり符号部と仮数部と指数部で表現される。

大学1年の時にやったことをかろうじて覚えてるだけなので復習。

[c]
#include <stdio.h>

void printbits(double v) {
  unsigned char *p = (unsigned char *)&v;
  int i;
  //リトルエンディアンのため後ろから見る
  for (i = 7; i >= 0; i--) {
    printf("%02x ", p[i]);
  }
  printf("\n");
}

int main(void){
  double d1 = 0.25;
  double d2 = 0.26;
  double result = d1+d2;

  printbits(d1);
  printbits(d2);
  printbits(result);
  printf("d1: %lf\n", d1);
  printf("d2: %lf\n", d2);
  printf("result: %lf\n", result);

  return 0;

}
[/c]

こいつを実行すると自分の環境では

3f d0 00 00 00 00 00 00
3f d0 a3 d7 0a 3d 70 a4
3f e0 51 eb 85 1e b8 52
d1: 0.250000
d2: 0.260000
result: 0.510000

という出力。

プログラムを見ればわかるけど、要はdoubleの値を浮動小数点数表示で見てる。

3f d0 00 00 00 00 00 00は2進表示で

00111111 11010000 00000000 00000000 00000000 00000000 00000000 00000000

フォーマットが何なのか知らないけど、多分IEEE754で符号部1bit, 指数部11bits, 仮数部52bits、ゲタ履きは1023

符号部は0なので+
指数部は2進の01111111101を10進に直して1021、ゲタを考慮するので1021-1023= -2
仮数部は0

よって
(-1) 符号部×2指数部 ‐1023×(1+仮数部)
=(-1) 0×2-2×(1+0)
=0.25

このようにちゃんと表現されてる。

循環小数である0.26も出力によると

3f d0 a3 d7 0a 3d 70 a4

0a3d7の繰り返しではなくて、最後が4と丸められている。丸め誤差。

今回は(多分)IEEE754だったけどあくまで数あるフォーマットの1つ。

応用情報の問題とかでは必ずしもIEEE754じゃないみたい。

とにかく浮動小数点数は上記のような感じ。

問題は固定小数点数!正直覚えてない!

そもそも事の発端はある人が言った言葉。

「金融系のアプリで丸め誤差は無いよ。COBOL使ってるから。COBOLは固定小数点数で、ある桁でズバッと切っちゃうからね」

あれ、固定小数点数とは言え循環小数は無理じゃね・・?

小数点の位置が固定だからって関係ないんじゃ・・・。

答えは調べたら簡単だった。

COBOLは2進化10進数(BCD)だから0.2とかも表現できる。

BCDとかやったはずなのにすっかり忘れてる。

BCD表現の0は0000で9は1001。

つまり13は0001 0011。

なので、13.2は0001 0011 . 0010ということみたい。

ああ、なるほど。これなら丸め誤差とか無いわ。各桁の重みが10だからなのね。

固定小数点数はただ単にある位置で小数点を固定、ってだけで浮動小数点数のようにきっちり覚える必要は無いようだった。

実装次第だろうけど、例えば0-31bitsある中で15と16の間に「.」があるとみなすとか、上16bits,16bitsで整数部、小数部を表現する、みたいな。

BCD表記で13.2をメモリに格納すると多分

00000000 00010011 00100000 00000000 (00 13 20 00)とか
00000000 00010011 00000000 00000010 (00 13 00 02)

って感じになってるんじゃないかな。これだと負数表現できねーし、実際どうなんだろう。。。

まあとにかく固定小数点数も復習できた。

結局COBOLは丸め誤差無いのは正しいみたいだけど、固定小数点表現だから丸めが無い、ってのは正しくないみたいだ。正しくはBCD表現だから。

実は今回の復習、いろいろやって合計4時間は費やした。

うーむ、悩ましい。

おまけ。

コードには%lfって書いてますけど戒めとして。ほんとは%lfなんてなくて、誤解が多すぎるので仕方なく追加された書式指定子。詳細は別サイトにお任せ。

じゃあなんでprintfの%fはfloatもdoubleも両方行けるのにscanfは%fと%lfで使い分ける必要があるのか、っていう話があるんですがそれはまた今度。

今回色々調べてる最中に見つけたサイト

http://0xcc.net/blog/archives/000164.html

アセンブリコードまで見てしっかり検証して行く所が俺とは大違いで素晴らしいですね。

凄く勉強になりました。long doubleが10ってのも


投稿日

カテゴリー:

投稿者:

タグ:

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です