統計的な丸め誤差の偏りを抑える round-to-even の解説と実装

制御装置のファームウェアなどで整数の値を丸める必要があるときに、
切捨てや四捨五入で丸めると 誤差の蓄積 が問題になる場合があります。
例えば、16ビットの測定データの結果を 10進数で 3桁に丸めて -99.9〜+99.9 の値と
して扱い、それを積算に用いるような場合が考えられます。
そのような場合は、
JIS Z 8401 規則A(ISO 80000-1、旧 ISO 31-0:1992 Annex B
"Guide to the rounding of numbers")にならった
偶数丸め(round-to-even)を用いる方が望ましいです。
また、2進整数の右シフトによる安易な 2の冪での除算は、実際には下位ビットの切り捨てとなるため、誤差の蓄積をもたらします。
この場合も偶数丸めと同様な考え方で扱うのが望ましいです。
10進数の偶数丸めと、四捨五入による誤差の蓄積の例
誤差の例として、入力が 20 から 0 へと順次変化した場合の丸めの例を Listing 1 に示します。
入力を積算した場合の値 210 に対して偶数丸めの場合には 210 と誤差が少なく(この場合は零に)なっていますが、 四捨五入での積算値は 220 と積算値が 5% 大きくなっています。 つまり四捨五入では、下位桁がその上の桁の半分の大きさのときに、 常に上側へ丸められます。 言い換えると、下位桁の半分だけ多く丸めているとも言えます。 このことによる統計的な誤差を軽減するために、下位桁がその上の桁の 半分の大きさ のときには上位桁が 偶数 に なるように丸めるのが偶数丸めです。
Listing 1 四捨五入と偶数丸めによる累積誤差の例入 力 四捨五入 偶数丸め
i = 20 j = 20 k = 20
i = 19 j = 20 k = 20
i = 18 j = 20 k = 20
i = 17 j = 20 k = 20
i = 16 j = 20 k = 20
i = 15 j = 20 k = 20
i = 14 j = 10 k = 10
i = 13 j = 10 k = 10
i = 12 j = 10 k = 10
i = 11 j = 10 k = 10
i = 10 j = 10 k = 10
i = 9 j = 10 k = 10
i = 8 j = 10 k = 10
i = 7 j = 10 k = 10
i = 6 j = 10 k = 10
i = 5 j = 10 k = 0
i = 4 j = 0 k = 0
i = 3 j = 0 k = 0
i = 2 j = 0 k = 0
i = 1 j = 0 k = 0
i = 0 j = 0 k = 0
-------------------------
計 210 220 210
10 で割る前提の整数値を偶数丸めする簡単なプログラム例を Listing 2 に示します。
Listing 2 整数の偶数丸めのプログラム例 (C言語の場合)
/* NAME
* _rnd2evn - round least significant digit
* SYNOPSIS
* int rnd2evn(int x)
* DESCRIPTION
* The rnd2evn() function return the nearest multiple-of-ten value of x.
* REFERENCE
* ISO 31-0:1992, Quantities and units - Part0 : General principles,
* Annex B (Guide to the rounding of numbers)
* SEE ALSO
* JIS Z 8401
* COPYRIGHT
* Copyright (c) 2009, Takayuki HOSODA. All rights reserved.
*/
int rnd2evn(int x) {
if (x > 0) {
x += 4 + (1 & (x / 10));
} else {
x -= 4 + (1 & (x / 10));
}
return x - (x % 10);
}
return 文のところ (list.2 中の橙色太字の箇所) を単に
return x / 10;
とすれば良いです。
unsigned int urnd2evn(unsigned int x) {
x += 5;
if (x % 20 == 10)
x--;
return x;
}
ISO C99 (ISO/IEC 9899:1999) 以降の C言語やその派生言語の処理系では、
標準ライブラリに round() 関数が用意されています。
以下は、古い処理系でそれが利用できない場合の代替実装です。
round() 関数を持たない ISO C99 より前の C言語の場合)
#include <math.h>
double
round(double x) {
double t;
if (isnan(x)) return(x);
if (isinf(x)) return(x);
if (x >= 0.0) {
t = floor(x);
if (t - x <= -0.5)
t += 1.0;
return (t);
} else {
t = floor(-x);
if (t + x <= -0.5)
t += 1.0;
return (-t);
}
}
整数の右シフトは 2の冪 による除算とみなされることが多いですが、 実際には下位ビットの切り捨てであり、数学的な除算とは異なります。 大抵の場合には偶数丸めを行った方が誤差の蓄積が出にくくて望ましいです。 誤差の例として、入力が 20 から 0 へと順次変化した場合の丸めの例を Listing 5 に示します。
Listing 5 2進数の2の冪での除算時の右シフトと偶数丸めによる累積誤差の例 (シフトは 1-bit 右シフト。偶数丸めは Listing 6 (n = 1) による。)
入力 , シフト, 偶数丸め
i = 20, j = 10, k = 10
i = 19, j = 9, k = 10
i = 18, j = 9, k = 9
i = 17, j = 8, k = 8
i = 16, j = 8, k = 8
i = 15, j = 7, k = 8
i = 14, j = 7, k = 7
i = 13, j = 6, k = 6
i = 12, j = 6, k = 6
i = 11, j = 5, k = 6
i = 10, j = 5, k = 5
i = 9, j = 4, k = 4
i = 8, j = 4, k = 4
i = 7, j = 3, k = 4
i = 6, j = 3, k = 3
i = 5, j = 2, k = 2
i = 4, j = 2, k = 2
i = 3, j = 1, k = 2
i = 2, j = 1, k = 1
i = 1, j = 0, k = 0
i = 0, j = 0, k = 0
----------------------
計 210, 100, 105
入力を積算した場合の値 210 の半分の 105 に対して 偶数丸めの場合の積算値は 105 と誤差が少なく(この場合は零に)なっていますが、 単なる 1-bit 右シフトの積算値では 100 と 5% 少なくなっています。 これは、右シフトによる除算は単なる下位ビットの切り捨てと同じだから当然と言えます。
正の整数の場合は次のように簡単になります。
Listing 6 正の整数 x の n bit 右シフト時の偶数丸めのプログラム例 (n bit 右シフト, C言語の場合)
/* unsigned right shift with round ties to even */
uint64_t uround(uint64_t x, uint8_t n) {
n &= 63; /*shift mask, 0 < n < 8 * sizeof(x) */
if (n == 0) return x;
return (x + ((1ULL << (n - 1)) - 1) + ((x >> n) & 1)) >> n;
}
Verilog などのハードウェア記述言語の場合は ビットスライス が出来ますので、シフト演算子を用いず次のように書くことができます。
Listing 7 正の整数 val の shr bit 右シフト時の偶数丸めのプログラム例 (shr bit 右シフト, Verilog の場合)
/* unsigned right shift with round ties to even */
/* Input data width=64, sh=0..63 */
localparam WBIT = 64;
function automatic [WBIT-1:0] uround;
input [WBIT-1:0] val;
input [5:0] shr; // 0..63
begin
if (shr == 0) begin
uround = val[WBIT-1:0];
end else begin
/* round bit = val[shr-1], tie bit = val[shr] */
uround = (val + (({WBIT{1'b1}} << (shr-1)) - 1) + val[shr]) [shr + WBIT - 1 -: WBIT];
end
end
endfunction
固定小数点数は、整数値に暗黙の小数点位置(スケール)を与えて実数を表現する方法で、 実際にはスケール付き整数として扱われます。
実際の計測システム等では速度や桁落ち防止等の観点から、データを浮動小数点と整数の狭間的な、 固定小数点の整数として扱う事も多い。 固定小数点では整数部と小数部のビット数をあらかじめ決めておき、整数演算として実数値を扱います。 このようなときも、例えば積算後に小数点位置のビットの何ビットか下位で丸めるといったことも行われます。 もちろん、この様な場合も偶数丸めが望ましいのですが、負の数の扱いや丸めるビットの位置に注意が必要です。
固定小数点整数では、値は「整数部」と「小数部」のビット列として表現されています。
Listing 8 では入力値 din を小数部のビット数 n_fp で右シフトすることで整数部
ip を求め、下位 n_fp ビットをマスクすることで小数部 fp を取り出しています。
また half は固定小数点表現での 0.5 を表します。
丸めの判定は次の規則で行われます。
符号付き整数の場合には負の数の扱いに注意が必要ですが、 Listing 8 では整数部の符号と小数部の値を判定することで 正負いずれの場合でも正しく偶数丸めが行われるようにしています。
/* round64 Rev.1.1 (2025-12-13)
* (c) 2009-2025 Takayuki HOSODA
* SPDX-License-Identifier: BSD-3-Clause
*/
#include <stdint.h>
int64_t round64(int64_t din, uint16_t n_fp);
/* round64 - round a signed 64-bit fixed-point number to the nearest integer with ties to even */
int64_t
round64(int64_t din, uint16_t n_fp) {
n_fp &= 0x3f; /* Limit fractional width to 0 to 63 bits */
if (n_fp == 0) return din; /* No fractional part: already integer */
int64_t ip = din >> n_fp; /* Arithmatic shirt right, two's complement */
uint64_t fp = (uint64_t)din & ((1LL << n_fp) - 1); /* Fractional part */
int64_t half = (1LL << (n_fp - 1)); /* 0.5 in fixed-point scale */
return (( ((ip & 1) && (fp == half))
|| ((ip >= 0) && (fp > half))
|| ((ip < 0) && (fp < half)))) ? (ip + 1) : ip;
}
#ifdef DEBUG_ROUND64
#include <stdio.h>
#include <stdint.h>
/* Assuming the round64 function is defined as previously discussed */
int64_t round64(int64_t din, uint16_t n_fp);
void demo_boundary(const char* label, int64_t din_q12) {
uint16_t test_n[] = {10, 11, 12}; /* Rounding bits */
int i;
printf(" [%s]\n", label);
printf(" Raw Q12 Value : 0x%08llX (%f)\n", (long long)din_q12, (double)din_q12/4096.0);
printf(" RoundOff | Target | Result Hex | Scaled Value | Note\n");
printf(" ---------+--------+------------+--------------+------------\n");
for (i = 0; i < 3; i++) {
uint16_t n = test_n[i];
int64_t res = round64(din_q12, n);
double val = (double)res / (double)(1LL << (12 - n));
/* Mask to 32-bit for cleaner hex display of 46-bit data */
printf(" %2u bits | Qn.%-2u | 0x%08X | %12g | ",
n, 12 - n, (unsigned int)(res & 0xFFFFFFFF), val);
/* Add descriptive note for the boundary condition */
if (n == 10) printf("Fraction < 0.5\n");
else if (n == 11) printf("Fraction = 0.5 (Ties)\n");
else printf("Fraction > 0.5\n");
}
printf("\n");
}
int main(void) {
/* 2.515625 (Q12) = 0x2840 (10304) */
/* -2.515625 (Q12) = 2's complement of 0x2840 */
demo_boundary("Positive Case: 2.515625", 0x00002840LL);
demo_boundary("Negative Case: -2.515625", -10304LL);
return 0;
}
#endif
固定小数点では整数部と小数部のビット数をあらかじめ決めて実数値を
整数として扱います。DSP や組み込み分野では、このビット配分を
Q形式 (Q-format) で表すことが多く、例えば
Qn.m は整数部 n bit、小数部 m bit を
持つ固定小数点表現を意味します。
また Q12 のような表記は小数部が 12 bit であることを表します。
Listing 8 のテストプログラムでは、固定小数点 Q12 形式の値
2.515625 と -2.515625 を例に、
丸め位置を変えた場合の結果を確認しています。
丸めによって小数部のビット数が変化するため、結果は
Qn.2、Qn.1、Qn.0 の形式で
表示しています。
小数部が 0.5 未満、ちょうど 0.5、0.5 より大きい場合の挙動を それぞれ示しており、0.5 のときに結果が偶数となる 「偶数丸め」が行われていることが確認できます。
[Positive Case: 2.515625]
Raw Q12 Value : 0x00002840 (2.515625)
RoundOff | Target | Result Hex | Scaled Value | Note
---------+--------+------------+--------------+------------
10 bits | Qn.2 | 0x0000000A | 2.5 | Fraction < 0.5
11 bits | Qn.1 | 0x00000005 | 2.5 | Fraction = 0.5 (Ties)
12 bits | Qn.0 | 0x00000003 | 3 | Fraction > 0.5
[Negative Case: -2.515625]
Raw Q12 Value : 0xFFFFFFFFFFFFD7C0 (-2.515625)
RoundOff | Target | Result Hex | Scaled Value | Note
---------+--------+------------+--------------+------------
10 bits | Qn.2 | 0xFFFFFFF5 | -2.75 | Fraction < 0.5
11 bits | Qn.1 | 0xFFFFFFFA | -3 | Fraction = 0.5 (Ties)
12 bits | Qn.0 | 0xFFFFFFFE | -2 | Fraction > 0.5
固定小数点演算では、単純な右シフトを 2 の冪による除算とみなして用いることがよくあります。 しかし実際には下位ビットの単なる切り捨てであり、数学的な意味での除算や丸めとは異なります。
下位ビットが一様分布していると仮定すると、各丸め方式の平均誤差は truncate / ceil / round-half-up / round-to-even で それぞれ -0.5 / +0.5 / +0.25 / 0 LSB となります。 IIR フィルタや積算処理などでは、この差が統計的な バイアスとして現れる場合があります。そのため必要に応じて Listing 8 のように丸め処理を明示的に行うことが望ましいでしょう。



© 2000 Takayuki HOSODA.