C#에 여러 번의 복식을 추가 할 때 순서가 반올림에 영향을 미치는 이유
-
22-08-2019 - |
문제
다음 c# 코드를 고려하십시오.
double result1 = 1.0 + 1.1 + 1.2;
double result2 = 1.2 + 1.0 + 1.1;
if (result1 == result2)
{
...
}
결과 1은 항상 결과 2와 동일해야합니까? 문제는 그렇지 않습니다. 결과 1은 3.3이고 결과 2는 3.3000000000000003입니다. 유일한 차이점은 상수의 순서입니다.
반올림 문제가 발생할 수있는 방식으로 복식이 구현된다는 것을 알고 있습니다. 절대 정밀도가 필요한 경우 대신 소수를 사용할 수 있다는 것을 알고 있습니다. 또는 if 문에서 math.round ()를 사용할 수 있습니다. 나는 C# 컴파일러가 무엇을하고 있는지 이해하고 싶어하는 괴상한 일입니다. 누구든지 나에게 말할 수 있습니까?
편집하다:
지금까지 Floating Point 산술을 읽거나 CPU가 복식을 처리하는 방법의 고유 한 부정확성에 대해 이야기 한 모든 분들께 감사드립니다. 그러나 나는 내 질문의 주요 추력이 여전히 답이 없다고 생각합니다. 그것은 올바르게 표현하지 않은 것에 대한 내 잘못입니다. 이렇게 넣어 드리겠습니다.
위의 코드를 세분화하면 다음 작업이 발생할 것으로 예상됩니다.
double r1 = 1.1 + 1.2;
double r2 = 1.0 + r1
double r3 = 1.0 + 1.1
double r4 = 1.2 + r3
위의 각 추가 각각에 반올림 오류가 있다고 가정 해 봅시다 (번호가 매겨진 E1..E4). 따라서 R1은 반올림 오류 E1을 포함하고 R2에는 반올림 오류 E1 + E2, R3에는 E3 + E4가 포함되어 있습니다.
이제 반올림 오류가 어떻게 발생하는지 정확히 알지 못하지만 E1+E2가 E3+E4와 동일 할 것으로 예상했을 것입니다. 분명히 그렇지는 않지만, 그것은 나에게 어떻게 든 잘못된 것 같습니다. 또 다른 것은 위의 코드를 실행할 때 반올림 오류가 발생하지 않는다는 것입니다. 그것이 CPU보다는 이상한 일을하는 C# 컴파일러라고 생각하게 만듭니다.
나는 내가 많은 것을 요구한다는 것을 알고있다. 그리고 누군가가 줄 수있는 가장 좋은 대답은 CPU 디자인에서 가서 박사 학위를하는 것입니다. 그러나 나는 단지 물어볼 것이라고 생각했습니다.
편집 2
원래 코드 샘플에서 IL을 살펴보면이 작업을 수행하는 CPU가 아닌 컴파일러라는 것은 분명합니다.
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 1
.locals init (
[0] float64 result1,
[1] float64 result2)
L_0000: nop
L_0001: ldc.r8 3.3
L_000a: stloc.0
L_000b: ldc.r8 3.3000000000000003
L_0014: stloc.1
L_0015: ret
}
컴파일러가 나를 위해 숫자를 추가하고 있습니다!
해결책
E1+E2가 E3+E4와 동일 할 것으로 예상했을 것입니다.
그것은 전적으로 기대하는 것과는 다릅니다
floor( 5/3 ) + floor( 2/3 + 1 )
동일하게
floor( 5/3 + 2/3 ) + floor( 1 )
바닥을 타기 전에 2^53을 곱하는 것을 제외하고.
12 비트 정밀 플로팅 포인트를 사용하고 값과 함께 자르기 :
1.0 = 1.00000000000 1.1 = 1.00011001100 1.2 = 1.00110011001 1.0 + 1.1 = 10.00011001100 // extended during sum r1 = 1.0 + 1.1 = 10.0001100110 // truncated to 12 bit r1 + 1.2 = 11.01001100101 // extended during sum r2 = r1 + 1.2 = 11.0100110010 // truncated to 12 bit 1.1 + 1.2 = 10.01001100110 // extended during sum r3 = 1.1 + 1.2 = 10.0100110011 // truncated to 12 bit r3 + 1.0 = 11.01001100110 // extended during sum r4 = r3 + 1.0 = 11.0100110011 // truncated to 12 bit
따라서 작업 순서/절단 순서를 변경하면 오류가 변경되고 R4! = R2가 변경됩니다. 이 시스템에서 1.1과 1.2를 추가하면 마지막 비트가 튀어 나와서 잘리지 않아야합니다. 1.0에서 1.1을 추가하면 마지막 비트 1.1이 손실되므로 결과는 동일하지 않습니다.
한 번의 순서로, 반올림 (자르기)은 후행을 제거합니다. 1
.
다른 순서에서, 반올림은 후행을 제거합니다. 0
두 번.
하나는 0과 같지 않습니다. 따라서 오류는 동일하지 않습니다.
복식에는 정밀도가 더 많아지고 C#은 자르기보다는 반올림을 사용하지만이 간단한 모델은 동일한 값의 다른 순서로 다른 오류가 발생할 수 있기를 바랍니다.
FP와 수학의 차이점은 +가 추가하기보다는 '추가 된 둥근'의 속기라는 것입니다.
다른 팁
C# 컴파일러는 아무것도하지 않습니다. CPU는입니다.
CPU 레지스터에 A가 있고 B를 추가하는 경우 해당 레지스터에 저장된 결과는 A+B이며 사용 된 부동 정밀도에 근접합니다.
C를 추가하면 오류가 추가됩니다. 이 오류 첨가는 전이 작업이 아니므로 최종 차이입니다.
보다 클래식 용지 (모든 컴퓨터 과학자가 부동 소수점 산술에 대해 알아야 할 것) 주제에. 이런 종류의 물건은 부동 소수점 산술에서 일어나는 일입니다. 컴퓨터 과학자는 1/3+1/3+1/3 그렇지 않아 1과 같다 ...
부동 소수점 작동 순서가 중요합니다. 질문에 직접 대답하지는 않지만 항상 부동 소수점 번호를 비교해야합니다. 관용을 포함시키는 것이 일반적입니다.
double epsilon = 0.0000001;
if (abs(result1 - result2) <= epsilon)
{
...
}
이것은 관심이있을 수 있습니다. 모든 컴퓨터 과학자가 부동 소수점 산술에 대해 알아야 할 것
순서에 따라 오류가 동일하지 않은 이유는 다른 예제로 설명 할 수 있습니다.
10 미만의 숫자의 경우 모든 숫자를 저장할 수 있으므로 1, 2, 3 등을 최대 10 개로 저장할 수 있지만 10 후에는 내부 손실로 인해 매번 두 번째 숫자 만 저장할 수 있습니다. 다시 말해서, 그것은 10, 12, 14 등만 저장할 수 있습니다.
이제이 예를 들어 다음이 다른 결과를 낳는 이유를 알 수 있습니다.
1 + 1 + 1 + 10 = 12 (or 14, depending on rounding)
10 + 1 + 1 + 1 = 10
부동 소수점 번호의 문제는 정확하게 표현할 수 없으며 오류가 항상 같은 방식으로 진행되는 것은 아니므로 순서가 중요하다는 것입니다.
예를 들어, 3.00000000003 + 3.00000000003은 6.00000000005 (끝에 6은 아님)가 될 수 있지만 3.000000003 + 2.99999999997은 6.00000000001 일 수 있습니다.
step 1: 3.00000000003 + 3.00000000003 = 6.00000000005
step 2: 6.00000000005 + 2.99999999997 = 9.00000000002
그러나 순서를 변경하십시오.
step 1: 3.00000000003 + 2.99999999997 = 6.00000000001
step 2: 6.00000000001 + 3.00000000003 = 9.00000000004
그래서 그것은 중요합니다.
물론, 당신은 위의 예가 서로 균형을 잡는다는 점에서 운이 좋을 것입니다. 첫 번째는 .xxx1에 의해, 다른 하나는 .xxx1에 의해 아래로 줄어들므로 둘 다 .xxx3을 제공하지만 보장은 없습니다.
중간 결과가 다르기 때문에 실제로 동일한 값을 사용하지 않습니다.
double result1 = 2.1 + 1.2;
double result2 = 2.2 + 1.1;
복식은 10 진수 값을 정확하게 표현할 수 없기 때문에 다른 결과를 얻습니다.