C言語:float型の精度

  1. long型からfloat型への変換で精度落ちするはずが
  2. float型変数の初期化
  3. 計算過程を追ってみる
  4. いろいろ試す
  5. アセンブラを見る
  6. おまけ

1. long型からfloat型への変換で精度落ちするはずが

藤原博文著「Cプログラミング専門過程」(技術評論社)のサンプルプログラムを試していたときであった。

  • C言語の式にfloat型とlong型が混在するとき、long型からfloat型へ格上げされて、計算が行われる。
  • float型の有効桁数は、6桁である。
  • long型は、-134217728~134217727が範囲である。
  • long型の範囲でかつ±106を超える範囲では、float型の精度は、long型の精度より低くなる。

したがって次のプログラムでは精度落ちが発生し、計算結果は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進表示を行っている。

結果は次のとおり。数値表現では1234567936.0, メモリ内容は 0x4e932c06 であった。ここでは、float型の精度に落ちている。

3. 計算過程を追ってみる

float1.cのプログラムの計算式、l = (lbig / ften) * ften の計算過程を各段階で値を表示する。

結果

(3)まで精度落ちしていない。計算途中はdouble型精度で計算していると推測できる。(4)でようやく精度落ちしている。変数f は double型→float型の精度落ちした値が保存されたと推測できる。それならば、(1)(3)でも、変数 f には精度落ちした値が保存されているはずだが、精度落ちしていない。

そういえば、printf のような引数の個数が可変の関数に float型が渡されるときは、double型に変換されてから渡される。計算途中はdouble型精度の一時変数が存在し、この一時変数が printf に渡されているとしたら、精度落ちしない表示がされる。そして一時変数からではなく、float型変数から読み出してきたとき、精度落ちした表示がされるのではないか?

例えばコンパイラが次のような実装だったとすれば。

このようなdouble型の一時変数は、一昔前のコンパイラである。いまどきのコンパイラは、float型のまま計算しているはずである。また「何らかの理由」が思いつかない。

4. いろいろ試す

もう一つ考えられるのが、浮動小数点プロセッサ内のレジスタが常にdouble型であること。一種の、グローバルな一時変数とでも言えるだろう。

「何らかの理由」は、直前で関数(この例ではprintf)を呼んでいることだろう。関数内で浮動小数点の計算をしていたら、double tmp; の値は変化しているだろうから。つまり計算の流れが続いているときは double tmp、すなわち浮動小数点プロセッサのレジスタの値を使う。計算の流れが断ち切られたら、本来のfloat変数を読みにいっているのではないか?

どうやら予想はあっていそうだ。

5. アセンブラを見てみる

念のため、アセンブラコードを見る。(実はアセンブラコードを見てから予想したのです)

  1. ViuaslCの[プロジェクト]-[設定…]を開く。
  2. [プロジェクトの設定]ダイアログの右側で、[C/C++]タブを開く。
  3. 中央の[カテゴリ]一覧から、”ファイルリスティング”を選択する。
  4. [リスティングファイルタイプ]一覧から、”ソースコードを含む”を選択する。
  5. [OK]で閉じて、コンパイルする。
  6. Debugフォルダ内の、拡張子asm が、アセンブラファイルである。

擬似コードで書き直すため、内部レジスタを 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年頃)