てくメモ

trivial な notes

【C#】2つの float が概ね等しいかを得る処理の確認

実用のうえで、近似した2つの浮動小数点数(この記事ではfloat)を等価として扱いたい場合がある。

Console.WriteLine(0.2f * 0.1f == 0.02f);
// False
// ⇧ 実用のうえで、True として扱ってほしい

記事現在、標準ではその機能を直接提供するメソッドはない。

一方、例えば Unity ではMathf.Approximatelyメソッドがあったり、標準ライブラリ内でもinternalで同様の処理が存在している。そのため、イディオムとして確認しておく。



方法の確認

参考先がある場合でもコードを必要に応じて書き換えている。
また、簡潔のため次のusing staticを行っている。

using static System.MathF;

素朴な方法

private static bool Simple(float a, float b, float epsilon)
{
    if (a == b) return true;

    return Abs(a - b) < epsilon;
}

差の絶対値を取り、それが許容誤差(ここでは微小数 = epsilon)より小さいかを返す。

C#において、無限の取り扱いを==と一致させる場合はif (a == b) return true;の部分が必要。

また、epsilon にfloat.Epsilonは使えない。これは、.NET の浮動小数点数に用意されているEpsilonフィールドは表現できる限界の微小数であり、丸め誤差の許容としては小さすぎるため1

Godot を参考

// 以下を参考
// https://github.com/godotengine/godot/blob/master/modules/mono/glue/GodotSharp/GodotSharp/Core/MathfEx.cs#L167
// 参照先 epsilon == 1e-06f
private static bool Godot(float a, float b, float epsilon)
{
    if (a == b) return true;

    float tolerance = epsilon * Abs(a); // Unity: Abs(a) ⇨ Max(Abs(a), Abs(b))
    if (tolerance < epsilon) tolerance = epsilon;

    return Abs(a - b) < tolerance;
}

素朴な方法は、対象の絶対値が大きい場合に期待する結果を得られない。そのため、許容誤差をスケールする。(相対誤差)

例ではゲームエンジンの Godot を参考先としたが、誤差を扱う部分については Unity のMathf.Approximatelyもだいたい同じ。

例ではスケールするための値を片方からしか取っていない(ゲームエンジンなので速度のため?)が、特に問題なければMax(Abs(a), Abs(b))が素直だと思われる。

PresentationCore を参考

// 以下を参考
// https://source.dot.net/#PresentationCore/src/Microsoft.DotNet.Wpf/src/Shared/MS/Internal/FloatUtil.cs,23cd7a9a418d008b
// 参照先 epsilon == 1.192092896e-07F
private static bool PresentationCore(float a, float b, float epsilon)
{
    if (a == b) return true;

    float tolerance = (Abs(a) + Abs(b) + 10.0f) * epsilon;

    float delta = a - b;
    return (-tolerance < delta) && (tolerance > delta);
}

標準の PresentationCore で用いられている別例。一部ライブラリ(e.g. Avalonia)もこれを参考にしたのか、同様だった。

許容誤差のスケール方法が異なり、また、差を比較する際に絶対値を取らない trick が用いられている。

値を流して確認

よくある、1.1, 1.01, 1.001... と流し込んでいき 1 と比較する、みたいなものを行う。

コードは一応以下に折りたたんでおく。

折りたたみ(長い。確認用)

// ※ 記事で使っていない部分もそのままコピペしている

private static bool Simple(float a, float b, float epsilon)
{
    if (a == b) return true;

    return Abs(a - b) < epsilon;
}

// 以下を参考
// https://github.com/godotengine/godot/blob/master/modules/mono/glue/GodotSharp/GodotSharp/Core/MathfEx.cs#L167
// 参照先 epsilon == 1e-06f
private static bool Godot(float a, float b, float epsilon)
{
    if (a == b) return true;

    float tolerance = epsilon * Abs(a); // Unity: Abs(a) ⇨ Max(Abs(a), Abs(b))
    if (tolerance < epsilon) tolerance = epsilon;

    return Abs(a - b) < tolerance;
}

// 以下を参考
// https://source.dot.net/#PresentationCore/src/Microsoft.DotNet.Wpf/src/Shared/MS/Internal/FloatUtil.cs,23cd7a9a418d008b
// 参照先 epsilon == 1.192092896e-07F
private static bool PresentationCore(float a, float b, float epsilon)
{
    if (a == b) return true;

    float tolerance = (Abs(a) + Abs(b) + 10.0f) * epsilon;

    float delta = a - b;
    return (-tolerance < delta) && (tolerance > delta);
}

private static (float lesser, float greater) GetThreshold(Func<float, float, float, bool> func, float epsilon)
{
    float lesser, greater;

    float i = 1f;
    while (true)
    {
        i *= 0.1f;
        var j = 1f + i;
        if (func(1f, j, epsilon))
        {
            lesser = j;
            break;
        }
    }

    i = 1f;
    while (true)
    {
        i *= 10f;
        var j = 1f + i;
        if (func(i, j, epsilon))
        {
            greater = j;
            break;
        }
    }

    return (lesser, greater);
}

private static (float lesser1, float lesser2, float greater1, float greater2) GetThresholdBothSide(Func<float, float, float, bool> func, float epsilon)
{
    float lesser1, lesser2, greater1, greater2;

    float i = 1f;
    while (true)
    {
        i *= 0.1f;
        var j = 1f + i;
        if (func(1f, j, epsilon))
        {
            lesser1 = j;
            break;
        }
    }

    i = 1f;
    while(true)
    {
        i *= 0.1f;
        var j = 1f + i;
        if (func(j, 1f, epsilon))
        {
            lesser2 = j;
            break;
        }
    }

    i = 1f;
    while (true)
    {
        i *= 10f;
        var j = 1f + i;
        if (func(i, j, epsilon))
        {
            greater1 = j;
            break;
        }
    }

    i = 1f;
    while (true)
    {
        i *= 10f;
        var j = 1f + i;
        if (func(j, i, epsilon))
        {
            greater2 = j;
            break;
        }
    }

    return (lesser1, lesser2, greater1, greater2);
}

private static string GetThresholdString(string label, Func<float, float, float, bool> func, float epsilon)
{
    var (l, g) = GetThreshold(func, epsilon);
    var (ls, gs) = (l.ToString(), g.ToString());
    var len = ls.Length - 2;
    return $"""
        ### {label}
        {ls} ≒ 1.{new string('0', (len < 0 ? 1 : len))}
        {gs} ≒ 1{new string('0', gs.Length - 1)}
        {GetThresholdPresentationCoreSpecificString(func, epsilon)}

        """;
}

private static string GetThresholdBothSideString(string label, Func<float, float, float, bool> func, float epsilon)
{
    var (l1, l2, g1, g2) = GetThresholdBothSide(func, epsilon);
    var (l1s, l2s, g1s, g2s) = (l1.ToString(), l2.ToString(), g1.ToString(), g2.ToString());
    var len1 = l1s.Length - 2;
    var len2 = l2s.Length - 2;
    return $"""
        ### {label}
        {l1s} ≒ 1.{new string('0', (len1 < 0 ? 1 : len1))}
        {l2s} ≒ 1.{new string('0', (len2 < 0 ? 1 : len2))}
        {g1s} ≒ 1{new string('0', g1s.Length - 1)}
        {g2s} ≒ 1{new string('0', g2s.Length - 1)}
        {GetThresholdPresentationCoreSpecificString(func, epsilon)}

        """;
}

private static string GetThresholdPresentationCoreSpecificString(Func<float, float, float, bool> func, float epsilon)
{
    const float specific = 1E+32f;
    return func(float.MaxValue, specific, epsilon)
            ? $"{float.MaxValue}{specific}"
            : $"{float.MaxValue}{specific}";
}

private static float GetThresholdPresentationCoreSpecific(float epsilon)
{
    float i = 1f;
    while (true)
    {
        i *= 10f;
        if (PresentationCore(float.MaxValue, i, epsilon)) return i;
    }
}

private static void Print(float epsilon)
{
    Console.WriteLine($"""
        {epsilon}
        --
        {GetThresholdString(nameof(Simple), Simple, epsilon)}
        {GetThresholdString(nameof(Godot), Godot, epsilon)}
        {GetThresholdString(nameof(PresentationCore), PresentationCore, epsilon)}
        """);
}

public static void Run()
{
    const float epsilon1 = 1e-06f;
    const float epsilon2 = 1.192092896e-07F;
    Print(epsilon1);
    Console.WriteLine();
    Print(epsilon2);
}


epsilon を 1E-06 / 1.192092896e-07F として確認したものを以下。

1E-06
--
### Simple
1.000001 ≒ 1.000000
100000000 ≒ 100000000
3.4028235E+38 ≠ 1E+32

### Godot
1.000001 ≒ 1.000000
10000001 ≒ 10000000
3.4028235E+38 ≠ 1E+32

### PresentationCore
1.00001 ≒ 1.00000
1000001 ≒ 1000000
3.4028235E+38 ≒ 1E+32
1.1920929E-07
--
### Simple
1 ≒ 1.0
100000000 ≒ 100000000
3.4028235E+38 ≠ 1E+32

### Godot
1 ≒ 1.0
10000001 ≒ 10000000
3.4028235E+38 ≠ 1E+32

### PresentationCore
1.000001 ≒ 1.000000
10000001 ≒ 10000000
3.4028235E+38 ≒ 1E+32

各メソッドの1行目は小数部の桁を増やしていってはじめてtrueとなった数。2行目は整数部の桁を増やしていってはじめてtrueとなった数。左の数の末尾の桁が1でない場合は、加算が情報落ちするまでtrueが返らなかったことを示す。

また、3行めは PresentationCore の実装をみてこうなるよな、というのを確認したもの。

簡単な確認だけれど、とりあえず言えそうなことは以下

  • エンジン・ライブラリ等で用意されていれば、なるべく乗っかるのを前提
  • 参考にするとして、epsilon は方法の参考先のものをそのまま使った方がよさそう(今回で言えば Godot の方法は 1E-06、PresentationCore の方法は 1.192092896e-07F)。
  • PresentationCore の方法では、1E+32 以降は true が返る


余談:名付けのバラツキ

この処理、決まった名前がついていないので名付けにバラツキがある。

主な軸は「概ね等しい」か「近い」か。以下は下調べしたときに見かけた一例

  • 前者:IsEqualApprox / EqualsRoughly / ToleranceType.Equals
  • 後者:AreClose

他言語でも isclose とか approxequal とかそういう感じの名付け。

Unity の Approximately は概ね等しい系統ではあるけれど、Equal が付いていないのは少し珍しい印象。



  1. 例えば1 + epsilon > 1Epsilonフィールドは満たさない。