コグノスケ


link 未来から過去へ表示(*)  link 過去から未来へ表示

link もっと前
2021年3月12日 >>> 2021年3月12日
link もっと後

2021年3月12日

浮動小数点数の丸め方

目次: C言語とlibc

C言語の規格では浮動小数点の丸めモードが4つ定義されています。実行中に変更することができ、fesetround() 関数で指定します。

ついUPWARD = 切り上げ、DOWNWARD = 切り捨て、と説明したくなりますが、TOWARDZEROと区別が付きませんし、正の数はまだしも、負の数を考えたとき混乱します。やや意味はわかりにくいですが、誤解のない説明をすると、こんな感じです。

FE_DOWNWARD
マイナス無限大方向への丸め、-11.5, -10.5, 10.5, 11.5を整数にすると、-12, -11, 10, 11
FE_TONEAREST
最も近い数への丸め
FE_TOWARDZERO
ゼロ方向への丸め、-11.5, -10.5, 10.5, 11.5を整数にすると、-11, -10, 10, 11
FE_UPWARD
プラス無限大方向への丸め、-11.5, -10.5, 10.5, 11.5を整数にすると、-11, -10, 11, 12

DOWNWARD, TOWARDZERO, UPWARDは他に解釈の余地がありませんが、TONEARESTはど真ん中の数が来たときに、扱いに困ります。IEEE 754では2つの方式が定義されているようです。

ties to even
偶数方向に丸める、10.5, 11.5を整数にすると10.0, 12.0になる
ties away from zero
ゼロから遠い方に丸める、10.5, 11.5を整数にすると11.0, 12.0になる、四捨五入に近い

説明するより実際に動かしたほうがわかりやすいでしょう。

丸めモードを実感

丸めモードの動作を確認するプログラムを書きます。8388610という数値はfloatの仮数部24bitを使い切った値となっています。1.0の増減は表現できますが、1.0より小さい値の増減はビットが足りないので表せないです。

8388610に対し1.0より小さい値(今回は0.25, 0.5, 0.75の3つを選びました)を加減算すれば、結果を正確に表現できないので、必ず丸め処理が行われます。丸めモードによる演算結果の違いを見るには丁度よいですね。

浮動小数点の丸めモードの動作を確認するプログラム

#include <stdio.h>
#include <fenv.h>
#include <float.h>

#define BASE_EVEN    8388610.0
#define BASE_ODD     8388611.0

union n_f {
	int n;
	float f;
};

static inline void test(void)
{
	union n_f a, b, c;

	a.f = BASE_EVEN;
	b.f = 0.25;
	c.f = a.f + b.f;
	printf("   %.2f+%.2f =  %.2f 0x%08x\n", a.f, b.f, c.f, c.n);
	b.f = 0.5;
	c.f = a.f + b.f;
	printf("   %.2f+%.2f =  %.2f 0x%08x\n", a.f, b.f, c.f, c.n);
	b.f = 0.75;
	c.f = a.f + b.f;
	printf("   %.2f+%.2f =  %.2f 0x%08x\n", a.f, b.f, c.f, c.n);

	a.f = BASE_ODD;
	b.f = 0.25;
	c.f = a.f + b.f;
	printf("   %.2f+%.2f =  %.2f 0x%08x\n", a.f, b.f, c.f, c.n);
	b.f = 0.5;
	c.f = a.f + b.f;
	printf("   %.2f+%.2f =  %.2f 0x%08x\n", a.f, b.f, c.f, c.n);
	b.f = 0.75;
	c.f = a.f + b.f;
	printf("   %.2f+%.2f =  %.2f 0x%08x\n", a.f, b.f, c.f, c.n);

	a.f = -BASE_EVEN;
	b.f = 0.25;
	c.f = a.f - b.f;
	printf("  %.2f-%.2f = %.2f 0x%08x\n", a.f, b.f, c.f, c.n);
	b.f = 0.5;
	c.f = a.f - b.f;
	printf("  %.2f-%.2f = %.2f 0x%08x\n", a.f, b.f, c.f, c.n);
	b.f = 0.75;
	c.f = a.f - b.f;
	printf("  %.2f-%.2f = %.2f 0x%08x\n", a.f, b.f, c.f, c.n);

	a.f = -BASE_ODD;
	b.f = 0.25;
	c.f = a.f - b.f;
	printf("  %.2f-%.2f = %.2f 0x%08x\n", a.f, b.f, c.f, c.n);
	b.f = 0.5;
	c.f = a.f - b.f;
	printf("  %.2f-%.2f = %.2f 0x%08x\n", a.f, b.f, c.f, c.n);
	b.f = 0.75;
	c.f = a.f - b.f;
	printf("  %.2f-%.2f = %.2f 0x%08x\n", a.f, b.f, c.f, c.n);
}

int main(int argc, char *argv[])
{
	printf("DOWNWARD\n");
	fesetround(FE_DOWNWARD);
	test();

	printf("TONEAREST\n");
	fesetround(FE_TONEAREST);
	test();

	printf("TOWARDZERO\n");
	fesetround(FE_TOWARDZERO);
	test();

	printf("UPWARD\n");
	fesetround(FE_UPWARD);
	test();

	return 0;
}

ところが、コンパイルして実行すると奇妙な結果が得られます。全てTONEARESTと同じ結果になってしまいます。

rounding-mathなし
$ gcc -Wall -g -O2 a.c -lm

$ ./a.out

DOWNWARD
   8388610.00+0.25 =  8388610.00 0x4b000002    ★TONEARESTと同じになっている
   8388610.00+0.50 =  8388610.00 0x4b000002
   8388610.00+0.75 =  8388611.00 0x4b000003
   8388611.00+0.25 =  8388611.00 0x4b000003
   8388611.00+0.50 =  8388612.00 0x4b000004
   8388611.00+0.75 =  8388612.00 0x4b000004
  -8388610.00-0.25 = -8388610.00 0xcb000002
  -8388610.00-0.50 = -8388610.00 0xcb000002
  -8388610.00-0.75 = -8388611.00 0xcb000003
  -8388611.00-0.25 = -8388611.00 0xcb000003
  -8388611.00-0.50 = -8388612.00 0xcb000004
  -8388611.00-0.75 = -8388612.00 0xcb000004
TONEAREST
   8388610.00+0.25 =  8388610.00 0x4b000002
   8388610.00+0.50 =  8388610.00 0x4b000002
   8388610.00+0.75 =  8388611.00 0x4b000003
   8388611.00+0.25 =  8388611.00 0x4b000003
   8388611.00+0.50 =  8388612.00 0x4b000004
   8388611.00+0.75 =  8388612.00 0x4b000004
  -8388610.00-0.25 = -8388610.00 0xcb000002
  -8388610.00-0.50 = -8388610.00 0xcb000002
  -8388610.00-0.75 = -8388611.00 0xcb000003
  -8388611.00-0.25 = -8388611.00 0xcb000003
  -8388611.00-0.50 = -8388612.00 0xcb000004
  -8388611.00-0.75 = -8388612.00 0xcb000004
TOWARDZERO
   8388610.00+0.25 =  8388610.00 0x4b000002    ★TONEARESTと同じになっている
   8388610.00+0.50 =  8388610.00 0x4b000002
   8388610.00+0.75 =  8388611.00 0x4b000003
   8388611.00+0.25 =  8388611.00 0x4b000003
   8388611.00+0.50 =  8388612.00 0x4b000004
   8388611.00+0.75 =  8388612.00 0x4b000004
  -8388610.00-0.25 = -8388610.00 0xcb000002
  -8388610.00-0.50 = -8388610.00 0xcb000002
  -8388610.00-0.75 = -8388611.00 0xcb000003
  -8388611.00-0.25 = -8388611.00 0xcb000003
  -8388611.00-0.50 = -8388612.00 0xcb000004
  -8388611.00-0.75 = -8388612.00 0xcb000004
UPWARD
   8388610.00+0.25 =  8388610.00 0x4b000002    ★TONEARESTと同じになっている
   8388610.00+0.50 =  8388610.00 0x4b000002
   8388610.00+0.75 =  8388611.00 0x4b000003
   8388611.00+0.25 =  8388611.00 0x4b000003
   8388611.00+0.50 =  8388612.00 0x4b000004
   8388611.00+0.75 =  8388612.00 0x4b000004
  -8388610.00-0.25 = -8388610.00 0xcb000002
  -8388610.00-0.50 = -8388610.00 0xcb000002
  -8388610.00-0.75 = -8388611.00 0xcb000003
  -8388611.00-0.25 = -8388611.00 0xcb000003
  -8388611.00-0.50 = -8388612.00 0xcb000004
  -8388611.00-0.75 = -8388612.00 0xcb000004

これはGCCの最適化による影響です。O1以上の最適化を行うと、演算時の丸めモードをTONEARESTと仮定し、コンパイル時に計算できる値を事前に計算する、という最適化が行われます。

この最適化が都合が良い場合もありますが、今回のようなプログラムでは丸めモードをTONEAREST以外に変更しているので、勝手にTONEARESTを仮定してはいけません。GCCの場合-frounding-mathというオプションを付けると、デフォルトの丸めモードを仮定した最適化をやめることができます。

rounding-mathあり
$ gcc -Wall -g -O2 a.c -lm -frounding-math

$ ./a.out

DOWNWARD    ★マイナス無限大方向に丸める
   8388610.00+0.25 =  8388610.00 0x4b000002    ★10.25 ->  10.00
   8388610.00+0.50 =  8388610.00 0x4b000002    ★10.50 ->  10.00
   8388610.00+0.75 =  8388610.00 0x4b000002    ★10.75 ->  10.00
   8388611.00+0.25 =  8388611.00 0x4b000003    ★11.25 ->  11.00
   8388611.00+0.50 =  8388611.00 0x4b000003    ★11.50 ->  11.00
   8388611.00+0.75 =  8388611.00 0x4b000003    ★11.75 ->  11.00
  -8388610.00-0.25 = -8388611.00 0xcb000003    ★-10.25 -> -11.00
  -8388610.00-0.50 = -8388611.00 0xcb000003    ★-10.50 -> -11.00
  -8388610.00-0.75 = -8388611.00 0xcb000003    ★-10.75 -> -11.00
  -8388611.00-0.25 = -8388612.00 0xcb000004    ★-11.25 -> -12.00
  -8388611.00-0.50 = -8388612.00 0xcb000004    ★-11.50 -> -12.00
  -8388611.00-0.75 = -8388612.00 0xcb000004    ★-11.75 -> -12.00
TONEAREST    ★最近接数に丸める、真ん中(0.5)は偶数側に丸める
   8388610.00+0.25 =  8388610.00 0x4b000002    ★10.25 ->  10.00
   8388610.00+0.50 =  8388610.00 0x4b000002    ★10.50 ->  10.00
   8388610.00+0.75 =  8388611.00 0x4b000003    ★10.75 ->  11.00
   8388611.00+0.25 =  8388611.00 0x4b000003    ★11.25 ->  11.00
   8388611.00+0.50 =  8388612.00 0x4b000004    ★11.50 ->  12.00
   8388611.00+0.75 =  8388612.00 0x4b000004    ★11.75 ->  12.00
  -8388610.00-0.25 = -8388610.00 0xcb000002    ★-10.25 -> -10.00
  -8388610.00-0.50 = -8388610.00 0xcb000002    ★-10.50 -> -10.00
  -8388610.00-0.75 = -8388611.00 0xcb000003    ★-10.75 -> -11.00
  -8388611.00-0.25 = -8388611.00 0xcb000003    ★-11.25 -> -11.00
  -8388611.00-0.50 = -8388612.00 0xcb000004    ★-11.50 -> -12.00
  -8388611.00-0.75 = -8388612.00 0xcb000004    ★-11.75 -> -12.00
TOWARDZERO    ★ゼロ方向に丸める
   8388610.00+0.25 =  8388610.00 0x4b000002    ★10.25 ->  10.00
   8388610.00+0.50 =  8388610.00 0x4b000002    ★10.50 ->  10.00
   8388610.00+0.75 =  8388610.00 0x4b000002    ★10.75 ->  10.00
   8388611.00+0.25 =  8388611.00 0x4b000003    ★11.25 ->  11.00
   8388611.00+0.50 =  8388611.00 0x4b000003    ★11.50 ->  11.00
   8388611.00+0.75 =  8388611.00 0x4b000003    ★11.75 ->  11.00
  -8388610.00-0.25 = -8388610.00 0xcb000002    ★-10.25 -> -10.00
  -8388610.00-0.50 = -8388610.00 0xcb000002    ★-10.50 -> -10.00
  -8388610.00-0.75 = -8388610.00 0xcb000002    ★-10.75 -> -10.00
  -8388611.00-0.25 = -8388611.00 0xcb000003    ★-11.25 -> -11.00
  -8388611.00-0.50 = -8388611.00 0xcb000003    ★-11.50 -> -11.00
  -8388611.00-0.75 = -8388611.00 0xcb000003    ★-11.75 -> -11.00
UPWARD    ★プラス無限大方向に丸める
   8388610.00+0.25 =  8388611.00 0x4b000003    ★10.25 ->  11.00
   8388610.00+0.50 =  8388611.00 0x4b000003    ★10.50 ->  11.00
   8388610.00+0.75 =  8388611.00 0x4b000003    ★10.75 ->  11.00
   8388611.00+0.25 =  8388612.00 0x4b000004    ★11.25 ->  12.00
   8388611.00+0.50 =  8388612.00 0x4b000004    ★11.50 ->  12.00
   8388611.00+0.75 =  8388612.00 0x4b000004    ★11.75 ->  12.00
  -8388610.00-0.25 = -8388610.00 0xcb000002    ★-10.25 -> -10.00
  -8388610.00-0.50 = -8388610.00 0xcb000002    ★-10.50 -> -10.00
  -8388610.00-0.75 = -8388610.00 0xcb000002    ★-10.75 -> -10.00
  -8388611.00-0.25 = -8388611.00 0xcb000003    ★-11.25 -> -11.00
  -8388611.00-0.50 = -8388611.00 0xcb000003    ★-11.50 -> -11.00
  -8388611.00-0.75 = -8388611.00 0xcb000003    ★-11.75 -> -11.00

丸めモードの切り替えが正常に働いている様子がわかります。またTONEARESTの丸めモードにした場合、ties to evenであることも観察できます。

C言語の規格は何と言っている?

C99のfinal draftをざっと見た限りでは、ties to evenかties away from zeroか、明確に書かれていないように見えました。規格的にはどちらなんでしょうね?実装依存なのかなあ?

編集者:すずき(2023/02/04 20:17)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



link もっと前
2021年3月12日 >>> 2021年3月12日
link もっと後

管理用メニュー

link 記事を新規作成

<2021>
<<<03>>>
-123456
78910111213
14151617181920
21222324252627
28293031---

最近のコメント5件

  • link 24年4月22日
    hdkさん (04/24 08:36)
    「うちのHHFZ4310は15年突破しまし...」
  • link 24年4月22日
    すずきさん (04/24 00:37)
    「ちゃんと数えてないですけど蛍光管が10年...」
  • link 24年4月22日
    hdkさん (04/23 20:52)
    「おお... うちのHHFZ4310より後...」
  • link 20年6月19日
    すずきさん (04/06 22:54)
    「ディレクトリを予め作成しておけば良いです...」
  • link 20年6月19日
    斎藤さん (04/06 16:25)
    「「Preferencesというメニューか...」

最近の記事3件

  • link 24年4月25日
    すずき (04/26 16:49)
    「[AVIFの変換] AVIFが読めないアプリケーションがたまにあるので、AVIF(AV1 Image File Format)...」
  • link 24年2月7日
    すずき (04/24 02:52)
    「[複数の音声ファイルのラウドネスを統一したい] PCやデジタル音楽プレーヤーで音楽を聞いていると、曲によって音量の大小が激しく...」
  • link 24年4月22日
    すずき (04/23 20:13)
    「[仕事部屋の照明が壊れた] いきなり仕事部屋のシーリングライトが消えました。蛍光管の寿命にしては去年(2022年10月19日の...」
link もっとみる

こんてんつ

open/close wiki
open/close Linux JM
open/close Java API

過去の日記

open/close 2002年
open/close 2003年
open/close 2004年
open/close 2005年
open/close 2006年
open/close 2007年
open/close 2008年
open/close 2009年
open/close 2010年
open/close 2011年
open/close 2012年
open/close 2013年
open/close 2014年
open/close 2015年
open/close 2016年
open/close 2017年
open/close 2018年
open/close 2019年
open/close 2020年
open/close 2021年
open/close 2022年
open/close 2023年
open/close 2024年
open/close 過去日記について

その他の情報

open/close アクセス統計
open/close サーバ一覧
open/close サイトの情報

合計:  counter total
本日:  counter today

link About www.katsuster.net
RDFファイル RSS 1.0

最終更新: 04/26 16:49