- long型からfloat型への変換で精度落ちするはずが
- float型変数の初期化
- 計算過程を追ってみる
- いろいろ試す
- アセンブラを見る
- おまけ
1. long型からfloat型への変換で精度落ちするはずが
藤原博文著「Cプログラミング専門過程」(技術評論社)のサンプルプログラムを試していたときであった。
- C言語の式にfloat型とlong型が混在するとき、long型からfloat型へ格上げされて、計算が行われる。
- float型の有効桁数は、6桁である。
- long型は、-134217728~134217727が範囲である。
- long型の範囲でかつ±106を超える範囲では、float型の精度は、long型の精度より低くなる。
したがって次のプログラムでは精度落ちが発生し、計算結果は1234567890にはならない(はず)。
/* float1.c : long型からfloat型へ格上げ時の精度落ち */ #include <stdio.h> main() { long lbig = 1234567890L; float ften = 10.0f; long l; /* 元の値 */ printf( "lbig: %ldn", lbig ); /* * l = (lbig / ften) * ften; * * の格上げの様子をキャスト演算子を使って示すと * * l = (long)( ((float)lbig / ften) * ften ); * * (float)lbig のキャスト時に、精度落ちが発生する */ l = (lbig / ften) * ften; printf( "l : %ldn", l ); } /*EOF*/
実行結果
lbig: 1234567890 l : 1234567890
精度が落ちると1234567936になっているはずが、精度落ちしていない!
浮動小数点プロセッサが普及していないころのCコンパイラは、計算途中ではfloat型を常にdouble型で計算を行い、変数に保存するときにfloat型で保存していた。VisualCもfloat型をdouble型として計算しているのだろうか?
2. float型変数の初期化
まず、変数の初期化と表示だけを行った。float型は、1234567890.0を保持できないため、1234567936.0 となる。そのときのメモリ内容は、数値の上位桁から 4e 93 2c 06である。
printf( "%08lx", *(long *)&f ); は、メモリ内容の16進表示を行っている。
/* float2.c : float型の 1234567890.0 の確認 */ #include <stdio.h> main() { float f1, f2; /* float型の定数で初期化する */ f1 = 1234567890.0f; printf( "f1: %f %08lxn", f1, *(long *)&f1 ); /* メモリ内容ズバリで初期化する */ *(long *)&f2 = 0x4e932c06; printf( "f2: %f %08lxn", f2, *(long *)&f2 ); } /*EOF*/
結果は次のとおり。数値表現では1234567936.0, メモリ内容は 0x4e932c06 であった。ここでは、float型の精度に落ちている。
f1: 1234567936.000000 4e932c06 f2: 1234567936.000000 4e932c06
3. 計算過程を追ってみる
float1.cのプログラムの計算式、l = (lbig / ften) * ften の計算過程を各段階で値を表示する。
/* float3.c : float1.cの段階を追う */ #include <stdio.h> main() { long lbig = 1234567890L; float ften = 10.0f; long l; float f; /* l = (lbig / ften) * ften; */ /* 予想(1) * long型からfloat型への変換で精度落ちする。 * 1234567936.0 */ f = lbig; printf( "(1) %fn", f ); /* 予想(2) * 計算式をdouble型で計算する。 * 123456789.0 */ f = lbig / ften; printf( "(2) %fn", f ); /* 予想(3) * 計算式をdouble型で計算するが、 * fに保存するとき度落ち。 * 1234567936.0 */ f = (lbig / ften) * ften; printf( "(3) %fn", f ); /* 予想(4) * 精度落ちしたものが復活するはずないので、 * 1234567936 */ l = f; printf( "(4) %ldn", l ); } /*EOF*/
結果
実際 予想 (1) 1234567890.000000 1234567936.0 × (2) 123456789.000000 123456789.0 ○ (3) 1234567890.000000 1234567936.0 × (4) 1234567936 12345678936 ○
(3)まで精度落ちしていない。計算途中はdouble型精度で計算していると推測できる。(4)でようやく精度落ちしている。変数f は double型→float型の精度落ちした値が保存されたと推測できる。それならば、(1)(3)でも、変数 f には精度落ちした値が保存されているはずだが、精度落ちしていない。
そういえば、printf のような引数の個数が可変の関数に float型が渡されるときは、double型に変換されてから渡される。計算途中はdouble型精度の一時変数が存在し、この一時変数が printf に渡されているとしたら、精度落ちしない表示がされる。そして一時変数からではなく、float型変数から読み出してきたとき、精度落ちした表示がされるのではないか?
例えばコンパイラが次のような実装だったとすれば。
main() { float f; double tmp; /* 一時変数 */ tmp = lbig; f = tmp; printf( "...", tmp ); tmp = lbig / ften; f = tmp; printf( "...", tmp ); l = f; /* l = tmp; なら精度落ちも小さいが、何らかの理由でそうではない */ printf( "...", l ); }
このようなdouble型の一時変数は、一昔前のコンパイラである。いまどきのコンパイラは、float型のまま計算しているはずである。また「何らかの理由」が思いつかない。
4. いろいろ試す
もう一つ考えられるのが、浮動小数点プロセッサ内のレジスタが常にdouble型であること。一種の、グローバルな一時変数とでも言えるだろう。
double tmp; /* 一時変数 */ main() { float f; tmp = lbig; f = tmp; printf( "...", tmp ); tmp = lbig / ften; f = tmp; printf( "...", tmp ); l = f; /* 何らかの理由で、 l = tmp; ではない */ printf( "...", l ); }
「何らかの理由」は、直前で関数(この例ではprintf)を呼んでいることだろう。関数内で浮動小数点の計算をしていたら、double tmp; の値は変化しているだろうから。つまり計算の流れが続いているときは double tmp、すなわち浮動小数点プロセッサのレジスタの値を使う。計算の流れが断ち切られたら、本来のfloat変数を読みにいっているのではないか?
/* float4.c : いろいろ試す */ #include <stdio.h> void func(void); void print_float( float f ); void print_double( double d ); main() { long l = 1234567890L; float f, g; f = l; printf( "%fn", f ); printf( "%fn", f ); /* 連続して printf */ printf( "n" ); f = l; g = f * 2.0; /* 別の計算を挿入 */ printf( "%fn", f ); printf( "n" ); f = l; func(); /* 関数を挿入 */ printf( "%fn", f ); printf( "n" ); f = l; print_double( f ); print_float( f ); printf( "n" ); f = l; print_float( f ); print_double( f ); printf( "n" ); f = l; print_double( f ); print_double( f ); printf( "n" ); } void func(void) { } void print_float( float f ) { printf( "%fn", f ); } void print_double( double d ) { printf( "%fn", d ); } /*EOF*/
どうやら予想はあっていそうだ。
float4.c 1234567890.000000 1234567936.000000 精度落ち 1234567936.000000 精度落ち 1234567936.000000 精度落ち 1234567890.000000 1234567936.000000 精度落ち 1234567936.000000 精度落ち 1234567936.000000 精度落ち 1234567890.000000 1234567936.000000 精度落ち
5. アセンブラを見てみる
念のため、アセンブラコードを見る。(実はアセンブラコードを見てから予想したのです)
- ViuaslCの[プロジェクト]-[設定...]を開く。
- [プロジェクトの設定]ダイアログの右側で、[C/C++]タブを開く。
- 中央の[カテゴリ]一覧から、"ファイルリスティング"を選択する。
- [リスティングファイルタイプ]一覧から、"ソースコードを含む"を選択する。
- [OK]で閉じて、コンパイルする。
- Debugフォルダ内の、拡張子asm が、アセンブラファイルである。
; 37 : ; 38 : f = l; fild DWORD PTR _l$[ebp] ....(1) fst DWORD PTR _f$[ebp] ....(2) ; 39 : print_double( f ); sub esp, 8 fstp QWORD PTR [esp] ....(3) call _print_double add esp, 8 ; 40 : print_double( f ); fld DWORD PTR _f$[ebp] ....(4) sub esp, 8 fstp QWORD PTR [esp] ....(5) call _print_double add esp, 8
擬似コードで書き直すため、内部レジスタを reg, スタック(関数の引数を渡すための領域)を stk とすると
(1)、reg = (double)l;
(2)、f = (float)reg; 精度落ち発生!
(3)、stk = reg;
(4)、reg = (float)f;
(5)、stk = reg;
(3)でレジスタの値をスタックに保存している、値の由来は(1)。
(5)でレジスタの値をスタックに保存している、値の由来は(4)、すなわち精度落ちした変数f。
6. おまけ
long型からfloat型への格上げ(?)の問題が、VisualCでの float型の扱いの確認になってしまった。
VisualC+Pentiumでは、float型の変数は、float型のまま扱われていることがわかった。コンパイラの最適化と浮動小数点プロセッサのおかげで、float型でも精度落ちが発生しにくいことがわかった。(コンパイルオプションでプロセッサを386/486に指定しても結果は同じだった)精度落ち「しにくい」であって「しない」わけではない。
変数のサイズは、float 4バイト、double 8バイトである。floatのメリットはサイズが小さいので、巨大配列が必要な場合はメモリを節約でき、転送時間が速い。
しかしC標準の算術関数は、引数も戻り値も double型ばかりである。float型<-->double型の型変換の警告はかなりうるさい。また引数を渡すときの float型<-->double型の型変換は、処理が遅くなってしまう可能性がある。
精度としては、通常はfloat型の精度で十分である。計算精度が問題になるような処理では当然double型を使うだろう。
初心者に浮動小数点を教えるときに、どちらから教えるかは悩みどころだ。筆者自身は特に理由がなければ、double型を使っている。
- float型
- printf/scanf で、両方 "%f" を使うことができる。
(printfにfloat型を渡すときは、自動的にdouble型に変換されている)
sin/cosなどの算術関数の引数はdouble型が多いので「型変換をしました」警告がうっとおしい。 - double型
- printfでは "%f"、scanfでは "%lf" と使い分ける必要がある。
(2000年頃)