実用のうえで、近似した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 + epsilon > 1
をEpsilon
フィールドは満たさない。↩