- 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年頃)