printfの丸め

ネット上で丸めについて検索すると、特にQ&Aサイトなどで見かける、
printf/sprintfの書式化で四捨五入する件について。
検証するテストソースは下のもの。
一応fesetroundはC99なので、
%>cc test.c -std=c99 -lm
としてコンパイルする。

#include <stdio.h>
#include <fenv.h>

int main(void)
{
double d[] = {0.05, 0.15, 0.25, 0.35, 0.45, 0.55
, 0.65, 0.75, 0.85, 0.95, 0.0};
int i;
for (i = 0; d[i] > 0.0; i++) {
printf("%.2f\t%.1f\t%.20f\n"
, d[i], d[i], d[i]);
}
fesetround(FE_UPWARD);
printf("\n");
for (i = 0; d[i] > 0.0; i++) {
printf("%.2f\t%.1f\t%.20f\n"
, d[i], d[i], d[i]);
}
return 0;
}

これを、Ubuntu12.04(32bit) - gcc 4.6.3 で実行してみる。

0.05 0.1 0.05000000000000000278
0.15 0.1 0.14999999999999999445
0.25 0.2 0.25000000000000000000
0.35 0.3 0.34999999999999997780
0.45 0.5 0.45000000000000001110
0.55 0.6 0.55000000000000004441
0.65 0.7 0.65000000000000002220
0.75 0.8 0.75000000000000000000
0.85 0.8 0.84999999999999997780
0.95 0.9 0.94999999999999995559

0.05 0.1 0.05000000000000000278
0.15 0.1 0.14999999999999999445
0.25 0.2 0.25000000000000000000
0.35 0.3 0.34999999999999997780
0.45 0.5 0.45000000000000001110
0.55 0.6 0.55000000000000004441
0.65 0.7 0.65000000000000002220
0.75 0.8 0.75000000000000000000
0.85 0.8 0.84999999999999997780
0.95 0.9 0.94999999999999995559

注目するのは、0.25, 0.75の二つ。
0.25/0.75共に2のべき乗で表現可能な数であって浮動小数点の内部表現で誤差が発生しない。
ここで0.25は0.2、0.75は0.8になっている。Banker's rounding(偶数丸め)となっていることが分かる。四捨五入ではない。
さらに、5を丸める際に、十進上のその桁のみを評価しているのではなくて下位桁も含めて、5かその大小かを判断している。なので、0.05は0.05...278と5より若干大きくなるので偶数奇数関係なく切り上げ対象となり0.1となる。
つまり近似値による誤差が含まれる場合(2のべき乗で表現できない場合)は問答無用で切り上げ/切り捨てされることとなり上記結果となる。
ちなみにUbuntuのこの版のgccではfesetroundは効かないようだ。

次に、FreeBSD 9.1Release(32bit) - gcc 4.2.1で実行してみる。

0.05 0.1 0.05000000000000000278
0.15 0.1 0.14999999999999999445
0.25 0.2 0.25000000000000000000
0.35 0.3 0.34999999999999997780
0.45 0.5 0.45000000000000001110
0.55 0.6 0.55000000000000004441
0.65 0.7 0.65000000000000002220
0.75 0.8 0.75000000000000000000
0.85 0.8 0.84999999999999997780
0.95 0.9 0.94999999999999995559

0.06 0.1 0.05000000000000000278
0.15 0.2 0.14999999999999999445
0.25 0.3 0.25000000000000000000
0.35 0.4 0.34999999999999997780
0.46 0.5 0.45000000000000001111
0.56 0.6 0.55000000000000004441
0.66 0.7 0.65000000000000002221
0.75 0.8 0.75000000000000000000
0.85 0.9 0.84999999999999997780
0.95 1.0 0.94999999999999995560

0.25、0.75を含めてUbuntuと同様の結果となり、下位桁含めた判定での偶数丸めであることがわかる。
FreeBSDのこの版ではfesetroundが効いていて、丸めの際に指定した丸めモードで丸められているため、小数点下一桁で書式化したものは期待したものとなっている。

ここまでは偶数丸めばかりだが、次にWindows7(64)、Eclipse上のMinGWで実行してみる。

0.05 0.1 0.05000000000000000300
0.15 0.1 0.14999999999999999000
0.25 0.3 0.25000000000000000000
0.35 0.3 0.34999999999999998000
0.45 0.5 0.45000000000000001000
0.55 0.6 0.55000000000000004000
0.65 0.7 0.65000000000000002000
0.75 0.8 0.75000000000000000000
0.85 0.8 0.84999999999999998000
0.95 0.9 0.94999999999999996000

0.05 0.1 0.05000000000000000300
0.15 0.1 0.14999999999999999000
0.25 0.3 0.25000000000000000000
0.35 0.3 0.34999999999999998000
0.45 0.5 0.45000000000000001000
0.55 0.6 0.55000000000000004000
0.65 0.7 0.65000000000000002000
0.75 0.8 0.75000000000000000000
0.85 0.8 0.84999999999999998000
0.95 0.9 0.94999999999999996000

当該桁より下位桁の状況で判定しているのは同じだが、0.25は0.3になっており、四捨五入となっている事がわかる。

データを取っていないが、VC++(2010)でも四捨五入となった。

と、ここまで書いておいて、では仕様上は、というと、
C89では、"The value is rounded to the appropriate number of digits."と、
適切な十進数に丸められるとだけ書かれ、丸め方法は書かれていない。
JIS-C(C99)では、丸めモードに従い丸められると書かれ、fesetroundが有効である場合、その丸めモードに依存する、のように書かれている 。
Linuxの64bit環境が今手元に無いのでsseではどうなるか不明なところだが、少なくとも387でのfesetroundはLinuxでは無効っぽい。さらに直近への丸めでも無く偶数丸めっぽい。

で、結論だが、printfの実数丸めは処理系依存のようで、近似値誤差の問題を別にしても、決して四捨五入とは限らないので、書式化で丸め、なんてこたぁやらないように。
スポンサーサイト

コメントの投稿

非公開コメント

プロフィール

f_yamaki

Author:f_yamaki

アクセスカウンタ
最近の記事
最近のコメント
最近のトラックバック
月別アーカイブ
カテゴリー
ブロとも申請フォーム

この人とブロともになる

ブログ内検索
RSSフィード
リンク