GAIO CLUB

2023年03月13日

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

静的解析/コンパイラ技術
いまさら聞けない 静的解析/コンパイラ技術
今回は 浮動小数点数の話です。

浮動小数点数に関するガイドラインでよく目にするルールは、"等価・非等価の判定をすべきでない"だと思います。他にもいくつか浮動小数点関連のルールがありますが、解りにくいものが多いように感じます。

今回はルールのお話をする前に、そもそも浮動小数点とは何者か、どんな特徴があるのか、お話ししたいと思います。

浮動小数点数の前に整数の話から

C言語の整数型には、符号の有無を除外すると、char・short・int・long・long longがあります。intやlongの大きさはコンパイラによって変わることがありますが、およそこんな感じです。
浮動小数点数の前に整数の話から
浮動小数点数の説明につなげるためにビットの値も書いておきます。signed char型の例です。
符号付き整数の場合、最上位ビットが1のデータはマイナスですから、プラス値は最上位ビットが0です。
浮動小数点数の前に整数の話から
この値は127です。1の位から64の位の全ビットが1なので、総和が127になります。
マイナス値は、プラス値の2の補数で表します。2の補数は、全ビットを反転して1足すことで求められます。

-127は、上図のビットを反転して1足したピットになります。
浮動小数点数の前に整数の話から
整数は右端に小数点の位置があり、小数点以下の値を持ちません。ここで、少しだけ小数の話をしておきます。

もし、127を表すビット列の中心に小数点(太線のところ)があったら、それぞれのビットの意味はどうなるでしょうか。
浮動小数点数の前に整数の話から
この値は7.9375です。小数点から上位方向は2倍のペースで位が上がりますが、下位方向へは1/2のペースで位が下がります。このビットの意味が浮動小数点数の基本になります。

浮動小数点数の話

C言語の浮動小数点型には、floatとdoubleがあります。long doubleをサポートしているコンパイラもあります。浮動小数点型は符号付きです。unsigned floatと書くとエラーになりますし、signed floatと書いてもエラーです。

浮動小数点の形式は、ほとんどのコンパイラが ieee754に準拠しています。しかし、 ieee754にはlong doubleの形式規定は無いようです。floatとdoubleはこんな感じです。
浮動小数点数の話
値の範囲について、一般的にはこのように表現されますが、これじゃ解らないですね。

それに、この範囲の値であれば表現できるような説明ですが、違います。この範囲のある条件を満たす値だけが正確に表現できて、それ以外の値は表現可能な近い値に変えられます。

つまり、整数は値の範囲をすべて表現できますが、浮動小数点数は値の範囲をすべて正確に表現できるわけではありません。小数点数はいくらでも細かく刻めるので、限られたビット数でほとんどの値を表現できないのは仕方ありません。

というわけで、ieee754の形式と表現できる値の関係を見ていきましょう。

ieee754フォーマット

ieee754の形式は以下の通りです。
ieee754フォーマット
細かいことは省き、ざっくり説明すると、Sビットは符号で、1が負、0が正です。整数のように、他のビットが正負によって変化することはありません。

仮数部には数値が入りますが、どんな値も1.xxxxという形で保存しています。例えば、先に出てきた7.9375の値も、小数点位置を最初の1のすぐ右にあるものとして保存します。
ieee754フォーマット
全ての数値を1.xxxxの形式で保存するため、1.xxxxの1は無くてもわかるので、この値は捨てて小数部のビット情報だけを仮数部に保存します。
ieee754フォーマット
floatの仮数部は23ビットですが、先頭の1は保存していないだけなので、実際に数値を表すビット数は24あることになります。指数部には、本当の値を求めるために何ビットシフトする必要があるかの値が入ります。

7.9375の場合、1.984375を左に2ビットシフトしなければならないので、2です。もし右にシフトするのなら、マイナス値が入ります(実際の指数部には、ある値(バイアス値)にシフト値を加算した値が入っています)。

この構造から、float型の変数が表現できる値は、24ビットで表現できる値であり、8ビットのシフト数で移動できる範囲の値となります。
ieee754フォーマット
同じように、double型は、53ビットで表現できる値で、11ビットのシフト数で移動できる範囲の値を表現できます。

floatの場合、8ビットでシフト可能な範囲が表現可能な範囲ですが、あくまでも2進数で表現した時の1が、連続する24ビット内に全てある数値しか正確に表せません。連続する24ビットの外に1があるような数値は不正確な値(表現可能な近くの値)になるのです。

表現可能な値とは

連続25ビット以上を使う値はどのような値でしょうか。

charは8ビット、shortは16ビット、これらの値はfloatで正確に表現できます。longが32ビットであれば、floatで表現できない値が表れます。25ビット以上を使う値、例えば16777217(0x1000001)はダメです。試してみましょう。

以下のプログラムを動かしてみます。
#include <stdio.h>
  int   I=16777217;
  float F;
  int main(void)
  {
    F = I;
    printf(" %d(0x%X) : %f\n",I,I,F);
    return 0;
  }
動作結果は、こうです。
16777217(0x1000001) : 16777216.000000
正確な値を持てないので、近くの値16777216になりました。
しかし、16777218なら、連続24ビットに収まるので保存可能です。もう一度試します。
#include <stdio.h>
  int   I=16777218;
  float F;
  int main(void)
  {
    F = I;
    printf(" %d(0x%X) : %f\n",I,I,F);
    return 0;
  }
動作結果。
16777218(0x1000002) : 16777218.000000
値の大小ではなく、連続ビットをどれくらい使うかが重要なのです。
大きな値でも連続24ビット内に有効ビット(1のビット)が収まるなら正確な値を保存できます。
#include <stdio.h>
  int   I=0x7FFFFF80;
  float F;
  int main(void)
  {
    F = I;
    printf(" %d(0x%X) : %f\n",I,I,F);
    return 0;
  }
動作結果。
2147483520(0x7FFFFF80) : 2147483520.000000
最後に、0.1から0.9まで、0.1刻みの値のうち正確に表現できる値がいくつあるか、お話しします。
実は正確に表現できるのは、0.5だけです。太線を小数点位置とすると、0.5の2進数表記はこのようになります。
表現可能な値とは
1のビットは1つしかありませんから、24ビット内に収まります。他の値はすべて、24ビットを超える位置に1があります。
表現可能な値とは
これらの値は、ビットがいくらあっても正確に表現できないので、仮数部23ビットのfloat型はもちろん、仮数部52ビットのdouble型でも正確に表現できません。

確認してみましょう。
多めの桁数で表示します。
#include <stdio.h>
  int main(void)
  {
    printf(" %40.40f\n",0.1);
    printf(" %40.40f\n",0.2);
    printf(" %40.40f\n",0.3);
    printf(" %40.40f\n",0.4);
    printf(" %40.40f\n",0.5);
    printf(" %40.40f\n",0.6);
    printf(" %40.40f\n",0.7);
    printf(" %40.40f\n",0.8);
    printf(" %40.40f\n",0.9);
    return 0;
  }
動作結果。
  0.1000000000000000055511151231257827021182
  0.2000000000000000111022302462515654042363
  0.2999999999999999888977697537484345957637
  0.4000000000000000222044604925031308084726
  0.5000000000000000000000000000000000000000
  0.5999999999999999777955395074968691915274
  0.6999999999999999555910790149937383830547
  0.8000000000000000444089209850062616169453
  0.9000000000000000222044604925031308084726
というわけで、ほとんどの小数値は正確に表現できないのです。


筆者紹介

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

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

開発1部 QTXグループ

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

人気のコラム

最新のコラム