Quantcast
Channel: 円周率近似値の日に生まれて理系じゃないわけないだろ! - knifeのblog
Viewing all articles
Browse latest Browse all 5376

long double型のprintfがよくわからない

$
0
0

久々のプログラミングの話しです。

ちょっと精度の良いプログラムが必要になったので、gccでプログラミングしていたのですが、浮動小数点型であるlong double型のprintfのフォーマットなんだっけ?
から始まって、
なんでこんな変な値になっちゃうの?
と頭が混乱してきてしまっています。

例えば、

float f;
double d;
long double l;


のように、float、double、long doubleの変数に対して小数点以下100桁の円周率、

 

f = 3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679;
d = 3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679;
l = 3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679;


のように代入したとする。

それぞれの変数をprintfで出力しようとするとき、どんなフォーマットにするのが正しいのだろうか。

今回は精度を知りたかったので、小数点以下がどんな感じになるのか、

printf("%.100f\n", f);
printf("%.100lf\n", d);
printf("%.100Lf\n", l);


を試した結果、
 

3.1415927410125732421875000000000000000000000000000000000000000000000000000000000000000000000000000000
3.1415926535897931159979634685441851615905761718750000000000000000000000000000000000000000000000000000
3.1415926535897932385128089594061862044327426701784133911132812500000000000000000000000000000000000000


となった。
赤文字が10進数的に正しい部分です。
floatからdoubleへの赤文字の桁数の伸びにくらべ、
doubleからlong doubleへの赤文字の桁数の伸びが悪い。
long doubleの精度が思いのほか悪いことが解った。

さて、綺麗に表示出来ているじゃないか?
と思われますが、実はここまでくるのが大変でした。
コンパイラオプションをいろいろと変えた結果、
-ansi
を付けると、上記の結果を得られたにすぎません。

因みに -ansi を付けずにコンパイルすると、
 

3.1415927410125732000000000000000000000000000000000000000000000000000000000000000000000000000000000000
3.1415926535897931000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-88796093704928900000000000000000000000000000.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000


こんな感じで、long double型だけがメチャクチャな値を出力しました。
小数点の位置がかなり右に行っているのと、そもそも値が負になってるし、…

-ansi オプションは絶対に必要なんだと、この段階では思ったんです。

で、次の段階に進むことにして、

printf("%.100f\n", sinf(f/4.0));
printf("%.100lf\n", sin(d/4.0));
printf("%.100Lf\n", sinl(l/4.0));


因みに、"%f" は 浮動小数点数型、"%lf"、"%Lf" といったようにビット数の違いによって書き分けたりするのだが、これがそもそも正しいのだろうか?
出力で、そんなフォーマットの書き分ける必要性があるのか?
例えば符号あり整数型では、short、int、long と8ビットから32ビットまであったとして、"%d" だけで事足りていたし、long long の64ビットだけ、"%lld" といった特別なフォーマットを使っていたように思う。
この流れだと、"%llf" といったフォーマットもあるんだろうという推測は出来るが、gccでどうなのかまではよく知らなかった。


-ansi オプションを付けてコンパイルし、実行してみると、

 

-88796088154006397672329879360059947087822848.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0.7071067811865474617150084668537601828575134277343750000000000000000000000000000000000000000000000000
0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000


sin(π/4)なので、sin(45˚)ですから、1/√2 なので、
double型しか正しい値を出力していない。
10進数的に正しいのは赤い文字だけです。

なぜ、ここにきて float型、long double型が壊れる?
sinf() の引数、戻り値は float、
sinl() の引数、戻り値は long double
で合ってるよね。

せっかくπの値の精度がでて、sinも精度が出るだろうと思ってたのに、俺何か悪いことしたんかな?

コンパイラオプションが悪いの?
printfのフォーマットが悪いの?
ソースにバグがあるの?

混乱してきてしまったのだが、一旦落ち着いて、いろいろと切り分けを始めた。


まずは、sizeofでそれぞれの型の大きさを調べよう。

printf("float: %d\n", sizeof(float));
printf("double: %d\n", sizeof(double));
printf("long double: %d\n", sizeof(long double));


これはさすがに問題ないよなぁ。

コンパイルオプションをいろいろと変えてみる。

-mlong-double-64 のとき、 
float: 4
double: 8
long double: 8


-mlong-double-80 のとき、
float: 4
double: 8
long double: 12


-mlong-double-128 のとき、
float: 4
double: 8
long double: 16


-ansi を付ける、付けないに関わらず、-mlong-double-x の x によって変化する。
x は 64、80、128を選べ、80がデフォルトのようだ。

sizeof の戻り値はバイト数ですから、実データの中身(ビット列)を見てみようと考えた。

というわけで、型宣言から考え直します。

union { float v,  long long int x; } f;
union { double v,  long long int x; } d;
union { long double v,  long long int x; } l;


これで、型宣言は終わり。
union なんて滅多に使わないから、忘れてましたよ。

変数 f.v, d.v, l.v に調べたい浮動小数点の値を入れ、f.x, d.x, l.x で中身を見てしまおうという算段です。

まずは解りやすい値をいれてみる。

f.v = 0.25f;
d.v = 0.25;
l.v = 0.25L;
printf("%016llx\n", f.x);
printf("%016llx\n", d.x);
printf("%016llx\n", l.x);


因みに "%llx" は、long long int や unsigned long long int の64ビット型整数を16進表示するためのものです。
016を付けることで、0パッティングをして、16桁で表示させます。

同様に、小数点以下100桁の円周率も入れてみる。

さて、結果はいかに。

まずは、-ansi オプションなしです。
 

 -mlong-double-64-mlong-double-80-mlong-double-128
2.50000000040200000
4004000000000000
4004000000000000
0000000040200000
4004000000000000
a000000000000000
0000000040200000
4004000000000000
0000000000000000
小数点以下
100桁の
円周率
0000000040490fdb
400921fb54442d18
400921fb54442d18
0000000040490fdb
400921fb54442d18
c90fdaa22168c000
0000000040490fdb
400921fb54442d18
8000000000000000


この16進数の結果から何が解るのかというと、mlong-double-80の値を16進から2進にしてみます。

00 00 00 00 40 20 00 00 (hex) -->
00000000 00000000 00000000 00000000 01000000 00100000 00000000 00000000 (bin)
00 00 00 00 40 49 0f db (hex) -->
00000000 00000000 00000000 00000000 01000000 01001001 00001111 11011011 (bin)


float型は、
赤文字が仮数部(23ビット)で、2.5と円周率のそれぞれの値なんだろう。
青文字が指数部(8ビット)で、どちらも10000000になっている。
戻り文字が符号(1ビット)で、どちらも0で正ということだろうと、何十年も前にやったであろうコンピュータ概論の授業を思い出しながら考えてみる。

40 04 00 00 00 00 00 00 (hex) -->
01000000 00000100 00000000 00000000 00000000 00000000 00000000 00000000 (bin)
40 09 21 fb 54 44 2d 18 (hex) -->
01000000 00001001 00100001 11111011 01010100 01000100 00101101 00011000 (bin)

double型は、
先のfloat型の円周率の仮数部100100100001111110…が、doubleと合致するところを探すと、
doubleの仮数部が52ビットと解り、指数部は11ビットとなった。
2.5と見比べても問題はないですね。

a0 00 00 00 00 00 00 00 (hex) -->
10100000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 (bin)
c9 0f da a2 21 68 c0 00 (hex) -->
11001001 00001111 11011010 10100010 00100001 01101000 11000000 00000000 (bin)


long double型、問題はここだ。
符号ビットが無くなっているし、指数部は1ビットなのだろうか?
仮数部は63ビットは間違いなさそうなんだけどね。

円周率のdoubleとlong doubleの仮数部を並べてみると、

 

1001001000011111101101010100010001000010110100011000
100100100001111110110101010001000100001011010001100000000000000


ビット数が11ビット増えているだけで、そこがオール0なんで、なんら精度が上がっていないことが解る。


では、-ansiオプションを付けてコンパイルしてみると、どうなるだろうか。
 

 -mlong-double-64-mlong-double-80-mlong-double-128
2.50000000040200000
4004000000000000
4004000000000000
0000000040200000
4004000000000000
a000000000000000
0000000040200000
4004000000000000
0000000000000000
小数点以下
100桁の
円周率
0000000040490fdb
400921fb54442d18
400921fb54442d18
0000000040490fdb
400921fb54442d18
c90fdaa22168c235
0000000040490fdb
400921fb54442d18
8469898cc51701b8


-mlong-double-64 では -ansi の有無で変化はない。

変化したところだけを抜き出してみます。

c9 0f da a2 21 68 c2 35 (hex) -->
11001001 00001111 11011010 10100010 00100001 01101000 11000010 00110101 (bin)


long double型の円周率の精度が11ビット分、何かしら埋まっていることが解ります。

さて、内部構造が見えてきましたね。
 

mlong-double-128 は、long long intでは桁が足りてないってことだろう。

union の long long int を char の配列とかにできたんだろうか。
まぁ、やってみよう。
 

union { float v; unsigned char x[4]; } f;
union { double v; unsigned char x[8]; } d;
union { long double v; unsigned char x[16]; } l;
int i, j;

f.v = 2.5f;
d.v = 2.5;
l.v = 2.5L;

for (i=sizeof(f.x)-1; i>=0; i--) for (j=7; j>=0; j--) printf("%d",(f.x[i]>>j)&1); printf("\n");
for (i=sizeof(d.x)-1; i>=0; i--) for (j=7; j>=0; j--) printf("%d",(d.x[i]>>j)&1); printf("\n");
for (i=sizeof(l.x)-1; i>=0; i--) for (j=7; j>=0; j--) printf("%d",(l.x[i]>>j)&1); printf("\n");

f.v = 3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679;
d.v = 3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679;
l.v = 3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679;

for (i=sizeof(f.x)-1; i>=0; i--) for (j=7; j>=0; j--) printf("%d",(f.x[i]>>j)&1); printf("\n");
for (i=sizeof(d.x)-1; i>=0; i--) for (j=7; j>=0; j--) printf("%d",(d.x[i]>>j)&1); printf("\n");
for (i=sizeof(l.x)-1; i>=0; i--) for (j=7; j>=0; j--) printf("%d",(l.x[i]>>j)&1); printf("\n");


ほぼコピペでこんな感じで書いてみた。
for文で書いたところはビット列を表示させるためです。

-mlong-double-128 -ansi
の両方のオプションを付けてコンパイルした結果、
 

01000000 00100000 00000000 00000000
01000000 00000100 00000000 00000000 00000000 00000000 00000000 00000000
01000000 00000000 01000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
01000000 01001001 00001111 11011011
01000000 00001001 00100001 11111011 01010100 01000100 00101101 00011000
01000000 00000000 10010010 00011111 10110101 01000100 01000010 11010001 10000100 01101001 10001001 10001100 11000101 00010111 00000001 10111000

※見にくいので8桁ずつに加工しました、実際の出力には色もスペースもありません。

いずれも符号ビットが0になってるし、指数部、仮数部も問題が無いだろう。
-ansi オプションを付けてコンパイルしているので、円周率の仮数部もしっかりと値が入っている。

円周率の精度の上げ方は、これで解った。

では、sin関数に円周率/4を渡して、sn(45˚)を計算させた結果が、なぜまともじゃないんだろうか。
 

f.v /= 4.0;
d.v /= 4.0;
l.v /= 4.0;

for (i=sizeof(f.x)-1; i>=0; i--) for (j=7; j>=0; j--) printf("%d",(f.x[i]>>j)&1); printf("\n");
for (i=sizeof(d.x)-1; i>=0; i--) for (j=7; j>=0; j--) printf("%d",(d.x[i]>>j)&1); printf("\n");
for (i=sizeof(l.x)-1; i>=0; i--) for (j=7; j>=0; j--) printf("%d",(l.x[i]>>j)&1); printf("\n");

f.v = sinf(f.v);
d.v = sin(d.v);
l.v = sinl(l.v);

for (i=sizeof(f.x)-1; i>=0; i--) for (j=7; j>=0; j--) printf("%d",(f.x[i]>>j)&1); printf("\n");
for (i=sizeof(d.x)-1; i>=0; i--) for (j=7; j>=0; j--) printf("%d",(d.x[i]>>j)&1); printf("\n");
for (i=sizeof(l.x)-1; i>=0; i--) for (j=7; j>=0; j--) printf("%d",(l.x[i]>>j)&1); printf("\n");


と先のプログラムにπ/4、sin(π/4)も追加してみる。
π/4の計算部分、
/= 4.0;
としたが、本当ならばシフト演算で、
>>= 2;
とやりたかったのだが、コンパイル時にエラーで弾かれてしまい、やむを得ず、/= 4.0; としました。


-ansi ありだと、

 

01000000 00100000 00000000 00000000
01000000 00000100 00000000 00000000 00000000 00000000 00000000 00000000
01000000 00000000 01000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
01000000 01001001 00001111 11011011
01000000 00001001 00100001 11111011 01010100 01000100 00101101 00011000
01000000 00000000 10010010 00011111 10110101 01000100 01000010 11010001 10000100 01101001 10001001 10001100 11000101 00010111 00000001 10111000
00111111 01001001 00001111 11011011
00111111 11101001 00100001 11111011 01010100 01000100 00101101 00011000
00111111 11111110 10010010 00011111 10110101 01000100 01000010 11010001 10000100 01101001 10001001 10001100 11000101 00010111 00000001 10111000
01001110 01111111 10100100 11001100
00111111 11100110 10100000 10011110 01100110 01111111 00111011 11001100
01000000 00011100 11111111 11110011 01001001 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

※見にくいので8桁ずつに加工しました、実際の出力には色もスペースもありません。
 

sin(π/4)のlong doubleが、精度的にみるとfloat並というのはどういう理屈なんだろうか?

シフト演算で>>=2とやろうとしたが、コンパイラに怒られたので、/=4でごましたのだが、πからπ/4への変化は、指数部だけの変化のみで、仮数部に変化が起きていないので、シフト演算をしていないのに、シフト演算と同じ結果となっているのは、コンパイラが優秀なのだろうか。
まぁ、/=4だから、それくらいはやってくれても当たり前でしょうか。

ただ、float、double、long doubleで指数部と仮数部に揺れがある。
この辺が混乱の元だったのだろうか。

-ansi なしだと、
 

01000000 00100000 00000000 00000000
01000000 00000100 00000000 00000000 00000000 00000000 00000000 00000000
01000000 00000000 01000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
01000000 01001001 00001111 11011011
01000000 00001001 00100001 11111011 01010100 01000100 00101101 00011000
01000000 00000000 10010010 00011111 10110101 01000100 01000010 11010001 10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00111111 01001001 00001111 11011011
00111111 11101001 00100001 11111011 01010100 01000100 00101101 00011000
00111111 11111110 10010010 00011111 10110101 01000100 01000010 11010001 10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00111111 00110101 00000100 11110011
00111111 11100110 10100000 10011110 01100110 01111111 00111011 11001100
00111111 11111110 10010010 00011111 10110101 01000100 01000010 11010001 10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

※見にくいので8桁ずつに加工しました、実際の出力には色もスペースもありません。

同様にπからπ/4へ、シフト演算をしていないが、指数部のみの変化で実質シフト演算されている。
こちらは、sin(π/4)の指数部が各変数の揺れがない。

円周率のlong doubleの仮数部はdouble並の精度しかないのだが、
sin(π/4)のlong doubleの仮数部もdouble並の精度になっている。


float = sinf(float);
double = sin(double);
long double = sinl(long double);


だよね。
なのに、関数への引数や戻り値の型は合ってても、戻り値の精度は悪いという結果なのだろうか。

結論としては、
コンパイラオプションに -ansi を付けるか付けないか。

-ansi オプションありでは、
直接値を代入したlong doubleの変数は精度が出る。
四則演算(実際は割り算しか試していないが)は精度に変化はなかった。
しかし、関数を通すと、関数の戻り値の型が double や long double であっても、float並の精度に落ちてしまった。

-ansi オプションなしでは、
直接値を代入したlong double変数の精度は、double並、
long doubleの戻り値も、double並。

ということが解った。

結局精度が出ないならば、
コンパイルオプションは、
-mlong-double-128 だけで、
-ansi は付けず、
プログラム内部でのprintfの出力では、
long doubuleの戻り値の関数は(double)にキャストして、"%lf" で出力すれば問題ないだろう。

どうしても中身を知りたい、精度の高い出力をしたいならば、今回私がやったようなunion型で、独自の出力を作るしかなかろうか。

因みに、
-mlong-double-128 -ansi
での円周率の仮数部を抜き出して、
多倍長演算で正確に計算させてみると、
 

float: 小数点以下22桁中10進6桁整合
3.1415927410125732421875
double: 小数点以下48桁中10進15桁整合
3.141592653589793115997963468544185161590576171875
long double: 小数点以下108桁中10進33桁整合
3.141592653589793238462643383279502797479068098137295573004504331874296718662975536062731407582759857177734375

 

のような値となり、赤文字部分が10進表記での等しい部分です。
printfの浮動小数のフォーマットで、これくらい出力出来ればいいんでしょうね。

 


ではでは


Viewing all articles
Browse latest Browse all 5376

Trending Articles