はじめに
Roundingについて説明します。Roundingとは"丸め"のことです。
ここでは次の処理によって整数を求める方法を"Rounding"とします。
- 浮動小数点数を整数にする
- 整数の除算
Directed rounding
ある方向に沿って整数に変換します。
- ∞ / + ∞ / 0に向かう / 0から遠ざかる それぞれの方向に応じて次の種類があります。
- R1 Round down / round towards minus infinity / floor (-∞)
- R2 Round up / round towards plus infinity / ceil (+∞)
- R3 Round towards zero / truncate / 切り捨て (->0<-)
- R4 Round away from zero / 切り上げ (<-0->)
横軸がRounding前、縦軸がRounding後のグラフを以下に示します。
斜めの点線はRoundingなしを表します。太線はRoundingありを表します。
次に具体例を示します。
R | example |
---|---|
R1(-∞) | -0.5, 0, 0.5 ⇒ -1, 0, 0 |
R2(+∞) | -0.5, 0, 0.5 ⇒ 0, 0, 1 |
R3(->0<-) | -0.5, 0, 0.5 ⇒ 0, 0, 0 |
R4(<-0->) | -0.5, 0, 0.5 ⇒ -1, 0, 1 |
Rounding to the nearest integer
最も近い整数に変換する方法です。四捨五入もこの方式の1つです。
隣り合う整数のちょうど中間の場合、最も近い整数が2つ存在します。
この場合、どちらの整数を選択するか決める必要があります。
例えば 0.5 に最も近い整数は 0 と 1 の 2つ あります。
中間の値をどの方向に向かって丸めるかという考え方をします。
- ∞ / + ∞ / 0に向かう / 0から遠ざかる / 偶数に向かう / 奇数に向かう
それぞれの方向に応じて次の種類があります。
- R5 Round half down (-∞)
- R6 Round half up (+∞)
- R7 Round half towards zero (->0<-)
- R8 Round half away from zero (四捨五入) (<-0->)
- R9 Round half to even (-> even <-)
- R10 Round half to odd (-> odd <-)
横軸がRounding前、縦軸がRounding後のグラフを以下に示します。
次に具体例を示します。
R | example |
---|---|
R5(-∞) | -0.5, 0, 0.5 ⇒ -1, 0, 0 |
R6(+∞) | -0.5, 0, 0.5 ⇒ 0, 0, 1 |
R7(->0<-) | -0.5, 0, 0.5 ⇒ 0, 0, 0 |
R8(<-0->) | -0.5, 0, 0.5 ⇒ -1, 0, 1 |
R9(even) | -1.5, -0.5, 0, 0.5, 1.5 ⇒ -2, 0, 0, 0, 2 |
R10(odd) | -1.5, -0.5, 0, 0.5, 1.5 ⇒ -1, -1, 0, 1, 1 |
Round half to even / odd には精度向上のメリットがあります。
具体例として 0.5 と 1.5 の合計を考えます。正しい値は2.0です。
0.5と1.5をそれぞれ四捨五入した後の合計を考えます。
0.5は1.0, 1.5は2.0になります。そのため合計は1.0+2.0=3.0となります。正しい値との差は1.0です。
0.5と1.5をそれぞれRound half to evenした後の合計を考えます。
0.5は0.0, 1.5は2.0になります。そのため合計は0.0+2.0=2.0となります。正しい値との差は0.0です。
C
CにおけるRoundingの振る舞いをみてみましょう。
MINGW64 の gcc で make します。
次のケースについて具体例を示します。
- 浮動小数点数を整数にする
- 整数の除算
- math.h (floor / ceil / round)
CFLAGS=-I. -g -Wall -Werror -O0
INCS=
OBJS=test.o
LIBS=
TARGET=test
%.o: %.c $(INCS)
$(CC) $(CFLAGS) -c -o $@ $<
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^ $(LIBS)
clean:
rm -rf $(TARGET) *.o
# include <stdio.h>
# include <math.h>
int main(int argc, char* argv[])
{
printf("casting\n");
printf(" (int) 1.0 = %+d\n", (int) 1.0);
printf(" (int) 0.9 = %+d\n", (int) 0.9);
printf(" (int) 0.5 = %+d\n", (int) 0.5);
printf(" (int) 0.1 = %+d\n", (int) 0.1);
printf(" (int) 0.0 = %+d\n", (int) 0.0);
printf(" (int)-0.1 = %+d\n", (int)-0.1);
printf(" (int)-0.5 = %+d\n", (int)-0.5);
printf(" (int)-0.9 = %+d\n", (int)-0.9);
printf(" (int)-1.0 = %+d\n", (int)-1.0);
printf("division\n");
printf(" 10/10 = %+d\n", 10/10);
printf(" 9/10 = %+d\n", 9/10);
printf(" 5/10 = %+d\n", 5/10);
printf(" 1/10 = %+d\n", 1/10);
printf(" 0/10 = %+d\n", 0/10);
printf(" -1/10 = %+d\n", -1/10);
printf(" -5/10 = %+d\n", -5/10);
printf(" -9/10 = %+d\n", -9/10);
printf(" -10/10 = %+d\n", -10/10);
printf("ceiling\n");
printf(" ceil( 1.0) = %+lf\n", ceil( 1.0));
printf(" ceil( 0.9) = %+lf\n", ceil( 0.9));
printf(" ceil( 0.5) = %+lf\n", ceil( 0.5));
printf(" ceil( 0.1) = %+lf\n", ceil( 0.1));
printf(" ceil( 0.0) = %+lf\n", ceil( 0.0));
printf(" ceil(-0.1) = %+lf\n", ceil(-0.1));
printf(" ceil(-0.5) = %+lf\n", ceil(-0.5));
printf(" ceil(-0.9) = %+lf\n", ceil(-0.9));
printf(" ceil(-1.0) = %+lf\n", ceil(-1.0));
printf("flooring\n");
printf(" floor( 1.0) = %+lf\n", floor( 1.0));
printf(" floor( 0.9) = %+lf\n", floor( 0.9));
printf(" floor( 0.5) = %+lf\n", floor( 0.5));
printf(" floor( 0.1) = %+lf\n", floor( 0.1));
printf(" floor( 0.0) = %+lf\n", floor( 0.0));
printf(" floor(-0.1) = %+lf\n", floor(-0.1));
printf(" floor(-0.5) = %+lf\n", floor(-0.5));
printf(" floor(-0.9) = %+lf\n", floor(-0.9));
printf(" floor(-1.0) = %+lf\n", floor(-1.0));
printf("rounding(Round half away from zero)\n");
printf(" round( 1.0) = %+lf\n", round( 1.0));
printf(" round( 0.9) = %+lf\n", round( 0.9));
printf(" round( 0.5) = %+lf\n", round( 0.5));
printf(" round( 0.1) = %+lf\n", round( 0.1));
printf(" round( 0.0) = %+lf\n", round( 0.0));
printf(" round(-0.1) = %+lf\n", round(-0.1));
printf(" round(-0.5) = %+lf\n", round(-0.5));
printf(" round(-0.9) = %+lf\n", round(-0.9));
printf(" round(-1.0) = %+lf\n", round(-1.0));
return 0;
}
$ gcc --version
gcc.exe (Rev2, Built by MSYS2 project) 6.2.0
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ make && ./test.exe
cc -I. -g -Wall -Werror -O0 -c -o test.o test.c
cc -I. -g -Wall -Werror -O0 -o test test.o
casting
(int) 1.0 = +1
(int) 0.9 = +0
(int) 0.5 = +0
(int) 0.1 = +0
(int) 0.0 = +0
(int)-0.1 = +0
(int)-0.5 = +0
(int)-0.9 = +0
(int)-1.0 = -1
division
10/10 = +1
9/10 = +0
5/10 = +0
1/10 = +0
0/10 = +0
-1/10 = +0
-5/10 = +0
-9/10 = +0
-10/10 = -1
ceiling
ceil( 1.0) = +1.000000
ceil( 0.9) = +1.000000
ceil( 0.5) = +1.000000
ceil( 0.1) = +1.000000
ceil( 0.0) = +0.000000
ceil(-0.1) = -0.000000
ceil(-0.5) = -0.000000
ceil(-0.9) = -0.000000
ceil(-1.0) = -1.000000
flooring
floor( 1.0) = +1.000000
floor( 0.9) = +0.000000
floor( 0.5) = +0.000000
floor( 0.1) = +0.000000
floor( 0.0) = +0.000000
floor(-0.1) = -1.000000
floor(-0.5) = -1.000000
floor(-0.9) = -1.000000
floor(-1.0) = -1.000000
rounding(Round half away from zero)
round( 1.0) = +1.000000
round( 0.9) = +1.000000
round( 0.5) = +1.000000
round( 0.1) = +0.000000
round( 0.0) = +0.000000
round(-0.1) = -0.000000
round(-0.5) = -1.000000
round(-0.9) = -1.000000
round(-1.0) = -1.000000
Intel processor
Intel processorは次のregisterでRoundingを指定します。
- x87 FPU control register (bits 10 and 11)
- The MXCSR register (bits 13 and 14)
この2bitをRCと表記します。
RC | Description |
---|---|
00b | Round half to even |
01b | Round down / round towards minus infinity / floor |
10b | Round up / round towards plus infinity / ceil |
11b | Round towards zero / truncate |
四捨五入(Round half away from zero)がないことがわかります。
SSE4.1からROUNDSD instructionが使用できます。
ROUNDSDはRC指定に従ってdoubleをRoundingします。
このinstructionは直接RCが指定できるため、MXCSRのRCを変更する必要がありません。
Round half away from zero(四捨五入)の計算
Round half away from zeroは次のように ±0.5 をした後に Round towards zero(切り捨て)
することで求めることができます。
# include <stdio.h>
int round_half_away_from_zero(double x)
{
double y;
if (0 < x) {
y = x + 0.5;
} else if (x < 0) {
y = x - 0.5;
} else {
y = x; /* x = 0 */
}
return (int) y; /* round towards zero; truncate */
}
int main(int argc, char* argv[])
{
printf("round_half_away_from_zero( 1.0) = %+d\n", round_half_away_from_zero( 1.0));
printf("round_half_away_from_zero( 0.9) = %+d\n", round_half_away_from_zero( 0.9));
printf("round_half_away_from_zero( 0.5) = %+d\n", round_half_away_from_zero( 0.5));
printf("round_half_away_from_zero( 0.1) = %+d\n", round_half_away_from_zero( 0.1));
printf("round_half_away_from_zero( 0.0) = %+d\n", round_half_away_from_zero( 0.0));
printf("round_half_away_from_zero(-0.1) = %+d\n", round_half_away_from_zero(-0.1));
printf("round_half_away_from_zero(-0.5) = %+d\n", round_half_away_from_zero(-0.5));
printf("round_half_away_from_zero(-0.9) = %+d\n", round_half_away_from_zero(-0.9));
printf("round_half_away_from_zero(-1.0) = %+d\n", round_half_away_from_zero(-1.0));
return 0;
}
$ make && ./test.exe
cc -I. -g -Wall -Werror -O0 -c -o test.o test.c
cc -I. -g -Wall -Werror -O0 -o test test.o
round_half_away_from_zero( 1.0) = +1
round_half_away_from_zero( 0.9) = +1
round_half_away_from_zero( 0.5) = +1
round_half_away_from_zero( 0.1) = +0
round_half_away_from_zero( 0.0) = +0
round_half_away_from_zero(-0.1) = +0
round_half_away_from_zero(-0.5) = -1
round_half_away_from_zero(-0.9) = -1
round_half_away_from_zero(-1.0) = -1
横軸(x)がRounding前、縦軸(y)がRounding後のグラフを以下に示します。
斜めの点線は round towards zero前のグラフです。太い点線はround towards zero後のグラフです。