C#(및 기타)에서 부동 소수점 숫자의 평등 비교
헐 박사
다음과 같이 C#에서 같음을 위해 두 개의 부동 소수점 숫자를 비교해야 합니다.
bool Equals(double x, double y, double tolerance)
{
var diff = Math.Abs(x - y);
return diff <= tolerance ||
diff <= Math.Max(Math.Abs(x), Math.Abs(y)) * tolerance;
}
tolerance
는 double.Epsilon
보다 훨씬 커야 합니다.항등 연산자의 문제
같음 연산자 ==를 사용하여 같은지 두 부동 소수점 숫자를 비교하는 것을 피해야 합니다. 이는 등호 연산자가 직관적으로 동일한 두 숫자에 대해 false를 반환할 수 있기 때문입니다. 다음 목록과 출력은 문제를 나타냅니다.
ar one = 0d;
for (var i = 0; i < 10; i++)
one += 0.1;
var pairs = new[]
{
(1.0 - 0.9, 0.1),
(0.15 + 0.15, 0.1 + 0.2),
(one, 1.0)
};
foreach (var p in pairs)
{
var (x, y) = p;
Console.WriteLine($"x: {x:f18}, y: {y:f18},\r\n " +
$"x == y: {x == y}, Abs(x-y): {Math.Abs(x - y):f18}");
}
x: 0.099999999999999978, y: 0.100000000000000006,
x == y: False, Abs(x-y): 0.000000000000000028
x: 0.299999999999999989, y: 0.300000000000000044,
x == y: False, Abs(x-y): 0.000000000000000056
x: 0.999999999999999889, y: 1.000000000000000000,
x == y: False, Abs(x-y): 0.000000000000000111
1.0 - 0.9와 0.1, 0.15 + 0.15와 0.1 + 0.2 사이의 동등 비교, 0.1과 1.0의 합은 예기치 않게 거짓이 됩니다. 부동 소수점 숫자의 오류로 인해 이러한 결과가 발생합니다.
절대 오류 허용
이 문제를 해결하는 잘 알려진 방법은 두 숫자의 절대 오차를 아주 작은 숫자와 비교하는 것입니다. 절대 오차가 1e-10보다 작기 때문에 다음 결과는 예상대로입니다.
const double tolerance = 1e-10;
foreach (var p in pairs)
{
var (x, y) = p;
var r = Math.Abs(x - y) <= tolerance;
Console.WriteLine($"x: {x:f18}, y: {y:f18},\r\n " +
$"TolerateAbsError: {r}, Abs(x-y): {Math.Abs(x - y):f18}");
}
x: 0.099999999999999978, y: 0.100000000000000006,
TolerateAbsError: True, Abs(x-y): 0.000000000000000028
x: 0.299999999999999989, y: 0.300000000000000044,
TolerateAbsError: True, Abs(x-y): 0.000000000000000056
x: 0.999999999999999889, y: 1.000000000000000000,
TolerateAbsError: True, Abs(x-y): 0.000000000000000111
이 접근 방식은 두 개의 큰 숫자 사이의 절대 오차가 작은 숫자를 쉽게 초과할 수 있는 문제가 있습니다. 예를 들어, 1e6에 0.1을 10번 더한 값과 1e6에 1.0을 더한 값을 비교하면 절대 오차가 1e-10을 초과하므로 거짓이 됩니다.
const double tolerance = 1e-10;
double x, y;
x = y = 1e6;
for (var i = 0; i < 10; i++)
x += 0.1;
y += 1.0;
var r = Math.Abs(x - y) <= tolerance;
Console.WriteLine($"x: {x:f18}, y: {y:f18},\r\n " +
$"TolerateAbsError: {r}, Abs(x-y): {Math.Abs(x - y):f18}");
x: 1000000.999999999767169356, y: 1000001.000000000000000000,
TolerateAbsError: False, Abs(x-y): 0.000000000232830644
상대 오차 허용
절대 오차 대신 상대 오차를 허용하면 이 문제를 해결할 수 있습니다. 두 숫자의 상대 오차는 절대 오차를 절대값의 최대값으로 나눈 값입니다.
1e6에 대한 두 덧셈 간의 상대 오차에 대한 다음 비교 허용 오차는 상대 오차가 1e-10보다 작기 때문에 true를 반환합니다.
a / b <= c
양식a <= b * c
을 참고하여 0으로 나누기를 피하십시오.x and y are the same as above.
var r = Math.Abs(x - y) <= Math.Max(Math.Abs(x), Math.Abs(y)) * tolerance;
var error = Math.Abs(x - y) / Math.Max(Math.Abs(x), Math.Abs(y));
Console.WriteLine($"x: {x:f18}, y: {y:f18},\r\n " +
$"TolerantRelativeError: {r}, RelativeError: {error:f18}");
x: 1000000.999999999767169356, y: 1000001.000000000000000000,
TolerantRelativeError: True, RelativeError: 0.000000000000000233
이 기술은 또 다른 문제를 일으킵니다. 거의 0과 같은 두 개의 작은 숫자 사이의 상대 오차는 매우 커질 수 있습니다. 예를 들어, 1e-11과 1e-12 사이의 상대 오차에 대한 비교 허용 오차는 false를 반환합니다.
const double tolerance = 1e-10;
var x = 1e-11;
var y = 1e-12;
var r = Math.Abs(x - y) <= Math.Max(Math.Abs(x), Math.Abs(y)) * tolerance;
var error = Math.Abs(x - y) / Math.Max(Math.Abs(x), Math.Abs(y));
Console.WriteLine($"x: {x:f18}, y: {y:f18},\r\n " +
$"TolerantRelativeError: {r}, RelativeError: {error:f18}");
x: 0.000000000010000000, y: 0.000000000001000000,
TolerantRelativeError: False, RelativeError: 0.900000000000000022
두 오류의 관계
동등 비교는 이러한 문제를 해결하기 위해 두 숫자의 절대 오차와 상대 오차를 모두 허용해야 합니다.
bool Equals(double x, double y, double tolerance)
{
var diff = Math.Abs(x - y);
return diff <= tolerance ||
diff <= Math.Max(Math.Abs(x), Math.Abs(y)) * tolerance;
}
위의 평등 비교는 두 코너 케이스 모두에 대해 true를 반환합니다.
const double tolerance = 1e-10;
double x, y;
bool r;
x = y = 1e6;
for (var i = 0; i < 10; i++)
x += 0.1;
y += 1.0;
r = Equals(x, y, tolerance);
Console.WriteLine($"x: {x:f18}, y: {y:f18},\r\n "+
$"TolerateRelativeAndAbsError: {r}");
x = 1e-11;
y = 1e-12;
r = Equals(x, y, tolerance);
Console.WriteLine($"x: {x:f18}, y: {y:f18},\r\n "+
$"TolerateRelativeAndAbsError: {r}");
x: 1000000.999999999767169356, y: 1000001.000000000000000000,
TolerateRelativeAndAbsError: True
x: 0.000000000010000000, y: 0.000000000001000000,
TolerateRelativeAndAbsError: True
기계 엡실론의 신화
지금까지 1e-10을
tolerance
임의로 선택했는데, 이 경우 적절한 값으로 오해되는 수학적으로 딱딱한 숫자가 있습니다. machine epsilon 입니다.많은 프로그래밍 언어는 형식적 정의가 다르더라도 기계 엡실론을 1과 그 다음으로 큰 부동 소수점 수의 차이로 정의합니다. C#에서
Double.Epsilon
에는 기계 엡실론이 있습니다.기계 엡실론을
tolerance
로 사용할 수 없습니다. Double.Epsilon
를 tolerance
로 채택하면 첫 번째 예에서 0.1과 1.0의 합을 비교하면 false가 반환됩니다. 기계 입실론은 너무 작고 산술 연산의 누적 오류가 쉽게 초과합니다.var one = 0d;
for (var i = 0; i < 10; i++)
one += 0.1;
var r = Equals(one, 1.0, Double.Epsilon);
Console.WriteLine($"x: {one:f18}, y: {1.0:f18},\r\n "+
$"UseMachineEpsilonAsTolerance: {r}");
x: 0.999999999999999889, y: 1.000000000000000000,
UseMachineEpsilonAsTolerance: False
결국
tolerance
는 애플리케이션에 필요한 정밀도를 기반으로 해야 합니다. 또한 기계 엡실론보다 훨씬 커야 합니다.결론
부동 소수점 숫자의 동등 비교는 절대 오차와 상대 오차를 모두 고려해야 합니다. 또한 비교의 오류 허용 오차는 애플리케이션에서 요구되는 정확도를 기반으로 선택해야 합니다. 이 주제에 대한 자세한 내용을 알고 싶다면 Comparing Floating Point Numbers, 2012 Edition 을 참조하십시오.
This GitHub repository은 위의 예와 이 기사를 기반으로
EqualityComparer<double>
및 IComperer<double>
의 구현을 가지고 있습니다.
Reference
이 문제에 관하여(C#(및 기타)에서 부동 소수점 숫자의 평등 비교), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/fujieda/equality-comparison-of-floating-point-numbers-in-c-and-others-3n3i텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)