GAIO CLUB

2023年04月03日

【第6回】理解しづらいコーディングガイドラインとは? ~浮動小数点数のガイドライン~

静的解析/コンパイラ技術
いまさら聞けない 静的解析/コンパイラ技術
前回は、浮動小数点数の形式(ieee754)と特徴、浮動小数点数値はほとんど不正確であるというお話をしました。

今回はこれをふまえて、いくつかのガイドラインを確認してみます。

浮動小数点の実装は、定義された浮動小数点規格に従う

説明によると、規格を明確にして規格に沿ったソフトを作成せよ、とあります。
さらに、異なるコンパイラの使用、異なる動作環境への移行では適正な処置をするようにとも書いてあります。そして、全てがieee754準拠とは限らないこと、丸め処理の違いなどがあげられています。

ここで、丸め処理についてお話しします。

前回、0.1は正確に表現できないというお話をしました。その際には0.1に近い別の値になると説明しましたが、別の値にするための方法が丸め処理です。ieee754では5つの方法が定義されており、どれを採用するかによって保存される値が変わります(0.1を5種類の値に丸める方法があるということではありません)。

前回お見せした0.1のビット列です。
浮動小数点の実装は、定義された浮動小数点規格に従う
黄色の部分は仮数ビットが足りないために保存できない部分です。このビットの状況によって、24bitsの最下位ビット(赤い0ビット)に1足すのか、足さないのかを判断する方法が5種類あります。
コンパイラの違いで丸め方法が異なりますし、同じコンパイラでも、加減乗除の演算ちがいで結果に対する丸め方法が異なることがあります。

浮動小数点数の限界を理解する

わざと選んでいるわけでは無いのですが、困ったガイドラインが続きます。「限界を理解する」です。
限界というのは、表現可能な範囲(最小~最大)と有効ビットが24ビット(floatの場合)である、ということだと思いますが、これを知っておく必要があるということですね。最小と最大はインクルードファイル内に定義されています。

有効ビット長の限界についてお話しします。
一つは精度の問題です。前回お話しした通り、0.1~0.9の間で正確な値を表せるのは0.5だけです。ですから、0.1を10回足しても1になりません。

0.1の加算を繰り返すと、少しずつですが、本当の値とのずれが広がります。
0.5なら正確に表せますから、もし可能であれば、5回に1度、0.5を足した値に補正すると、ずれの広がりを防ぐことができます。

精度問題に近いのですが、届かないビットへの操作という問題もあります。値の差がとても大きい値どうしの加算を行うと、演算処理が無意味になることがあります。
たとえば、以下の例です。
#include <stdio.h>
  float F = 4294967296.0f;  // 0x100000000
  int main(void)
  {
    printf("before : %f¥n",F);
    F += 100.0f;
    printf("after : %f¥n",F);
    return 0;
  }
動作結果。
before	: 4294967296.000000
after	: 4294967296.000000
4294967296(0x100000000) に100を加算しても変化しません。
100は決して小さな値ではありませんが、この2つの値には24ビットを超える差があります。
浮動小数点数の限界を理解する
24ビットからこぼれた下位のビットの部分に加算しても、変数に保存される24ビットの値は変化しません。

同じ理由で、Fの値を0から少しずつ増加させた場合、途中までは順調に増えてゆきますが、あるところでFの増加は止まります。その値は、floatの最大値とは関係なく、加算する2つの値のビット表現上の差が原因です。

しかし、加算結果の丸め処理が「切り上げ」で行われたら、赤字のビットが1になり、不正確ながらも加算結果が得られます。

整数型から浮動小数点型への変換時に精度を確保する

整数から浮動小数点に変換する時に精度が落ちる可能性があるのは以下の場合です。

・long → float
・longlong → float
・longlong → double

これは有効ビット長を基準にした話で、整数の値が浮動小数点型に十分収まる程度に小さければ精度は落ちません。精度が落ちる原因は値を表すビットの長さです。
floatの仮数は23ビットです。これに「隠しビット」と呼ばれる最上位ビットを加えた24ビットに値を保存します。同様にdoubleは52+1の53ビットに保存します。これらのビット数に収まらない整数値を代入すると、下位ビットがこぼれるので丸め処理が行われます。

longをいったんfloatに入れてlongに戻す例を作ってみました。
#include <stdio.h>
    long L = 0x0AAAAAAA;
    float F;
    int main(void)
    {
      printf(" L : %d (0x%08X)¥n",L,L);
      F = L;
      printf(" F : %f <----- 24ビット化、丸め処理¥n",F);
      L = F;
      printf(" L : %d (0x%08X)¥n",L,L);
      return 0;
    }
動作結果。
L : 178956970 (0x0AAAAAAA)
F : 178956976.000000 <----- 24ビット化、丸め処理
L : 178956976 (0x0AAAAAB0)
longをfloatに代入した時に24ビット精度に丸められます。

こぼれたビット部分の最後のAが消えて、丸め処理の影響で、ひとつ前のAがBに変化します。
このfloatをlongに代入すると仮数部の24ビットはそのままlongに入りますが、それ以下のビットはゼロが埋められます。

別の値で試してみます。
#include <sstdio.h>
    long L = 0x0FFFFFFF;
    float F;
    int main(void)
    {
      printf(" L : %d (0x%08X)¥n",L,L);
      F = L;
      printf(" F : %f <----- 24ビット化、丸め処理¥n",F);
      L = F;
      printf(" L : %d (0x%08X)¥n",L,L);
      return 0;
    }
動作結果。
L : 268435455 (0x0FFFFFFF)
F : 268435456.000000 <----- 24ビット化、丸め処理
L : 268435456 (0x10000000)
この例では、下位4ビットだけでなく多くのビットが0になっています。

これは、float代入時の丸め処理が最下位ビットに1を足したとき、桁上がりが発生したためです。

浮動小数点型複合式は、より小さな型へのキャストだけが許される

こちらも、例を作りました。
#include <stdio.h>
  float F1 = 4294967296.0f;	  // 0x100000000
  float F2 = 100.0f;	          // 0x000000064
  float  F;
  double D;
  int main(void)
  {
    printf("    F1 : %12.1f¥n",F1);
    printf("    F2 : %12.1f¥n",F2);
    F = F1 + F2;
    printf(" 1. F : %13.1f <--- F1 + F2¥n",F);
    D = (double)(F1 + F2);	// 大きな型へのキャストはルール違反
    printf(" 2. D : %13.1f <--- (double)(F1 + F2)  NG ¥n",D);
    D = (double)F1 + F2;
    printf(" 3. D : %13.1f <--- (double)F1 + F2¥n",D);
    return 0;
  }
動作結果。
  F1 : 4294967296.0
  F2 :        100.0
1. F :  4294967296.0 <--- F1 + F2
2. D :  4294967296.0 <--- (double)(F1 + F2)  NG 
3. D :  4294967396.0 <--- (double)F1 + F2
この加算式は、最初の例と同じ24ビットからこぼれ落ちる計算です。
しかし、doubleで計算すれば有効ビットが53ビットありますから、ビットはこぼれ落ちません。

1の結果は、floatサイズで加算しています。
2の結果は、加算式をdoubleへキャストしており、本ルールに違反します。式の結果を大きな型にキャストするのはNGです。
3の結果は、片方の変数をdoubleにキャストした後に加算しているので、doubleサイズの加算となり、100を加算できています。

浮動小数点式は、等価又は非等価の比較をしない

多くの場合に正確な値を持てず、丸め処理が働いてしまうので、==!=の判定をしてはいけないというものです。

丸め処理が同じように行われれば、等価判定も可能と思われるかもしれませんが、コンパイラの違い、演算子の違い、演算ライブラリ等の違いにより丸め処理が異なることがあります。

浮動小数点型変数は、ループ カウンタとして使用しない

ルールの説明には誤差の発生を理由にあげていますが、「限界を理解する」のところに記載した通り、ループカウンタが変化しなくなる可能性もあります。

浮動小数点の例外値を検査する

浮動小数点数には特別な値があります。
ieee754で特別にビットパターンが定義されています。

・INF(infinity)
指数ビットが全部1で、仮数ビットは全部0です。
表現可能な最大値を超えてしまうと、この値になります。
正と負があります。

・NaN(Not a Number)
指数ビットが全部1で、仮数ビットの一部または全部が0以外です。
演算結果が未定義な演算をした時に発生します。
例えば、0.0 / 0.0の割り算で発生することがありますが、NaNにならないコンパイラもあります。
正と負があります。

・0
指数部も仮数部も全部0です。
浮動小数点の0には正と負があります(マイナスゼロがあります)。

浮動小数点数に関するルールは他にもいろいろありますが、今回はieee754の構造から、いくつかのルールを考えてみました。

筆者紹介

浅野 昌尚(あさの まさなお)

ガイオ・テクノロジー株式会社

開発1部 QTXグループ

1980年代から30年以上にわたり汎用構造のCコンパイラ開発に従事し、その間に8ビットマイコンからRISC・VLIW・画像処理プロセッサまで、さまざまなCPU向けのクロスCコンパイラを開発。

人気のコラム

最新のコラム