浮動小数点による演算で、意図した結果と異なるのはなぜか


情報落ちについての質問です。

以下のプログラムで `1/n` を `n` 回加算しているのに、答えが `1.0` にならない理由を教えてください。

[code]

    float f = 0.0; 
    int n = 100000;
    
    for(int i = 0; i < n; i++) { 
        f += 1.0/n;
    }
    
    println("f = " + f);
    
[/code]




厳密に説明すると難しくなるのでたとえ話を含めて

「１０進数表記で正確に表現できる数」であっても「２進数表記で正確に表現できない」ものは（提示の `1/100000`
はまさにこれ）２進数表記では循環小数になります。コンピュータのメモリは有限なので無限桁数を扱うことはできません。なのでどこか適当なところで打ち切る必要があります。すると真の値とは必ず誤差が生じます。これを打ち切り誤差と言います。

それは例えば `1/3` を「３進数表記」すれば正確に表現できるのに対して「１０進数表記したら循環小数になる」のと同じです。 `0.33333`
で打ち切った数値は正確に `1/3` ではありません。打ち切った結果として真の値と異なってしまいます。

さて、これ (`1/3` ないしは `0.33333`) を加えていくことを考えます。

`1/3 + 1/3 + 1/3` を３進数表記したら `0.1 + 0.1 + 0.1` ですから `1` が得られて正確な結果となります（`float`
がそうであるようなコンピュータはありませんが）

１０進数表記で `0.33333 + 0.33333 + 0.33333` だと結果は `0.99999` になりそうです。よって打ち切り誤差
**だけが** 累積する場合は、正の数の和の結果は真の値より必ず小さくなります。

コンピュータ内部でこの手の浮動小数点数を扱う際には打ち切り誤差とは別に「丸め誤差」が生じます。丸め誤差とは「正しく表現できない数を、正しく内部表現できる数で代用する場合に、どの数を選ぶか」で生じます。先ほどの
`1/3` は `0.3333....` 無限桁は表現できないので `0.33333` で打ち切ったわけです。では `2/3` を `0.66666`
とするか `0.66667` とするかは案件・要望によって異なります。より誤差の小さい数値に丸めた結果は元の数値より大きい値になることがあります。

提示例は「打ち切って小さくなる」「丸めて大きくなる」の両方が生じた結果、たまたま大きい方向になったという代物です。

[java](/questions/tagged/java "'java' のタグが付いた質問を表示") でこの制御ができるかどうか知らんので
[c](/questions/tagged/c "'c' のタグが付いた質問を表示") のサンプル `MSVC` だと、この辺の「丸め方向」を
`_controlfp()` で制御できます。今どき流に `_s` なブツを使ってみると

[code]

    #include <stdio.h>
    #include <float.h>
    int main() {
        unsigned int fp;
        _controlfp_s(&fp, 0, 0);
        _controlfp_s(&fp, _RC_CHOP, _MCW_RC);
        float f = 0.0;
        int n = 100000;
    
        for (int i = 0; i < n; i++) {
            f += 1.0 / n;
        }
    
        printf("f = %g\n", f);
    }
    
[/code]

上記例題で `_RC_CHOP` は「０方向への丸め」（絶対値が小さい最近値へ丸める）ですが、こうすると結果は `0.997263` と小さくなります。
`_RC_NEAR` は「最も近い値へ丸め」（標準設定）で、これだと結果は `1.00099` と大きくなります。既に回答・コメントにある
`n=104857` の場合とか `_RC_UP` とか `_RC_DOWN`
とか、加算する数が負の時どうなるかとかをいろいろ試すと理解が深まると思います。

