目次: C言語とlibc
C言語の規格では浮動小数点の丸めモードが4つ定義されています。実行中に変更することができ、fesetround() 関数で指定します。
ついUPWARD = 切り上げ、DOWNWARD = 切り捨て、と説明したくなりますが、TOWARDZEROと区別が付きませんし、正の数はまだしも、負の数を考えたとき混乱します。やや意味はわかりにくいですが、誤解のない説明をすると、こんな感じです。
DOWNWARD, TOWARDZERO, UPWARDは他に解釈の余地がありませんが、TONEARESTはど真ん中の数が来たときに、扱いに困ります。IEEE 754では2つの方式が定義されているようです。
説明するより実際に動かしたほうがわかりやすいでしょう。
丸めモードの動作を確認するプログラムを書きます。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と同じ結果になってしまいます。
$ 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というオプションを付けると、デフォルトの丸めモードを仮定した最適化をやめることができます。
$ 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であることも観察できます。
C99のfinal draftをざっと見た限りでは、ties to evenかties away from zeroか、明確に書かれていないように見えました。規格的にはどちらなんでしょうね?実装依存なのかなあ?
< | 2021 | > | ||||
<< | < | 03 | > | >> | ||
日 | 月 | 火 | 水 | 木 | 金 | 土 |
- | 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 | - | - | - |
合計:
本日:
管理者: Katsuhiro Suzuki(katsuhiro( a t )katsuster.net)
This is Simple Diary 1.0
Copyright(C) Katsuhiro Suzuki 2006-2023.
Powered by PHP 8.2.15.
using GD bundled (2.1.0 compatible)(png support.)