합계 순서를 변경하면 다른 결과가 반환되는 이유는 무엇입니까?


294

합계 순서를 변경하면 다른 결과가 반환되는 이유는 무엇입니까?

23.53 + 5.88 + 17.64 = 47.05

23.53 + 17.64 + 5.88 = 47.050000000000004

JavaJavaScript는 모두 동일한 결과를 반환합니다.

부동 소수점 숫자가 이진수로 표시되는 방식으로 인해 일부 합리적인 숫자 ( 예 : 1/3-0.333333 ... )를 정확하게 표현할 수 없음을 이해합니다.

요소의 순서를 변경하면 결과에 영향을주는 이유는 무엇입니까?


28
실수의 합은 연관적이고 교환 적입니다. 부동 소수점은 실수가 아닙니다. 실제로 당신은 그들의 작업이 정식 적이 지 않다는 것을 증명했습니다. 그것들이 너무 연관되어 있지 않다는 것을 보여주는 것은 매우 쉽습니다 (예 :) (2.0^53 + 1) - 1 == 2.0^53 - 1 != 2^53 == 2^53 + (1 - 1). 따라서, 예 : 합계 및 기타 작업의 순서를 선택할 때주의하십시오. 일부 언어는 "고정밀"합계 (예 : python 's math.fsum) 를 수행하기위한 내장 기능을 제공 하므로 순진 합계 알고리즘 대신 이러한 함수를 사용하는 것이 좋습니다.
Bakuriu

1
@RBerteig 산술 표현식에 대한 언어의 연산 순서를 검사하여 판별 할 수 있으며 메모리에서 부동 소수점 숫자의 표현이 다르지 않은 경우 연산자 우선 순위 규칙이 동일하면 결과가 동일합니다. 참고 사항 : 은행 응용 프로그램을 개발하는 개발자가이를 파악하는 데 얼마나 오래 걸 렸는지 궁금합니다. 추가 0000000000004 센트가 실제로 합산됩니다!
Chris Cirefice

3
@ChrisCirefice : 0.00000004 센트 가 있다면 잘못하고 있습니다. 재무 계산에 이진 부동 소수점 유형을 사용 해서는 안됩니다 .
Daniel Pryden

2
@DanielPryden Ah 아아, 그것은 농담이었습니다 ...이 유형의 문제를 정말로 해결 해야하는 사람들이 아는 가장 중요한 일 중 하나가 사람들의 통화 상태를 유지한다는 아이디어를 던졌습니다. . 나는 매우 냉소적이었다 ...
크리스 Cirefice

6
매우 건조하고 오래되었지만 여전히 관련성이 있음 : 모든 컴퓨터 과학자가 부동 소수점 산술에 대해 알아야 할 사항
Brian

답변:


276

어쩌면이 질문은 어리석은 일이지만 왜 요소의 순서를 변경하면 결과에 영향을 미칩니 까?

크기에 따라 값이 반올림되는 지점이 변경됩니다. 의 예를 들어 어떤 우린 보는이의 대신 이진 부동 소수점, 우리는 각 추가가 "무한"정밀도로 수행 한 후 반올림 4 개 유효 숫자와 소수점 형 부동 소수점을 사용하고 척하자 그 일의 가장 가까운 표현 가능한 숫자 다음은 두 가지 합계입니다.

1/3 + 2/3 + 2/3 = (0.3333 + 0.6667) + 0.6667
                = 1.000 + 0.6667 (no rounding needed!)
                = 1.667 (where 1.6667 is rounded to 1.667)

2/3 + 2/3 + 1/3 = (0.6667 + 0.6667) + 0.3333
                = 1.333 + 0.3333 (where 1.3334 is rounded to 1.333)
                = 1.666 (where 1.6663 is rounded to 1.666)

우리는 이것이 문제가되기 위해 비 정수가 필요하지 않습니다.

10000 + 1 - 10000 = (10000 + 1) - 10000
                  = 10000 - 10000 (where 10001 is rounded to 10000)
                  = 0

10000 - 10000 + 1 = (10000 - 10000) + 1
                  = 0 + 1
                  = 1

이것은 중요한 부분이 소수 자릿수가 아닌 유효 숫자 가 제한적이라는 것을보다 명확하게 보여줍니다 . 항상 같은 수의 소수점 이하 자릿수를 유지할 수 있다면 적어도 덧셈과 뺄셈을 사용하면 값이 오버플로되지 않는 한 괜찮을 것입니다. 문제는 더 큰 숫자에 도달하면 더 작은 정보가 손실된다는 것입니다.이 경우 10001은 10000으로 반올림됩니다. (이것은 Eric Lippert가 그의 답변에서 지적한 문제의 예입니다 .)

오른쪽 첫 번째 줄의 값이 모든 경우에 동일하다는 점에 유의해야합니다. 따라서 10 진수 (23.53, 5.88, 17.64)가 정확히 double값 으로 표시되지 않는다는 것을 이해해야합니다. 위에 표시된 문제로 인해 문제가 발생했습니다.


10
May extend this later - out of time right now!간절히 기다리고 @Jon
Prateek

3
나중에 답변으로 돌아갈 것이라고 말할 때 커뮤니티는 나에게 약간 덜 친절합니다.
Grady Player

2
@ 종 정리 (ZongZhengLi) : 그것을 이해하는 것이 중요하지만,이 경우 근본 원인은 아닙니다. 당신은 값으로 비슷한 예를 작성할 수 있습니다 정확히 바이너리 표현을, 같은 효과를 볼. 여기서 문제는 대규모 정보와 소규모 정보를 동시에 유지하는 것입니다.
Jon Skeet

1
@Buksy : 10000으로 반올림 -4 자리의 유효 숫자 만 저장할 수있는 데이터 유형을 다루기 때문입니다. (so x.xxx * 10 ^ n)
Jon Skeet 2011

3
@meteors : 아니요, 오버플로가 발생하지 않으며 잘못된 숫자를 사용하고 있습니다. 10001은 10000으로 반올림되지만 1001은 1000으로 반올림되지 않습니다. 더 명확하게하기 위해 54321은 54320으로 반올림됩니다. 왜냐하면 4 자리의 유효 숫자 만 있기 때문입니다. "4 자리 유효 숫자"와 "최대 값 9999"사이에는 큰 차이가 있습니다. 내가 전에 말했듯이, 당신은 기본적으로 표현하고 x.xxx * 10 ^ N, 10000, x.xxx는 1.000 것, n은이처럼 4. 것이다 어디 doublefloat어디에서 매우 많은 수의 연속적인 표현 가능한 숫자, 1 개 이상 떨어져 있습니다.
Jon Skeet

52

바이너리로 진행되는 일이 있습니다. 우리가 알다시피, 일부 부동 소수점 값은 정확히 10 진수로 표현 될 수 있다고하더라도 바이너리로 정확하게 표현 될 수 없습니다. 이 세 숫자는 그 사실의 예일뿐입니다.

이 프로그램으로 각 숫자의 16 진수 표현과 각 덧셈의 결과를 출력했습니다.

public class Main{
   public static void main(String args[]) {
      double x = 23.53;   // Inexact representation
      double y = 5.88;    // Inexact representation
      double z = 17.64;   // Inexact representation
      double s = 47.05;   // What math tells us the sum should be; still inexact

      printValueAndInHex(x);
      printValueAndInHex(y);
      printValueAndInHex(z);
      printValueAndInHex(s);

      System.out.println("--------");

      double t1 = x + y;
      printValueAndInHex(t1);
      t1 = t1 + z;
      printValueAndInHex(t1);

      System.out.println("--------");

      double t2 = x + z;
      printValueAndInHex(t2);
      t2 = t2 + y;
      printValueAndInHex(t2);
   }

   private static void printValueAndInHex(double d)
   {
      System.out.println(Long.toHexString(Double.doubleToLongBits(d)) + ": " + d);
   }
}

printValueAndInHex방법은 16 진수 프린터 도우미입니다.

출력은 다음과 같습니다.

403787ae147ae148: 23.53
4017851eb851eb85: 5.88
4031a3d70a3d70a4: 17.64
4047866666666666: 47.05
--------
403d68f5c28f5c29: 29.41
4047866666666666: 47.05
--------
404495c28f5c28f6: 41.17
4047866666666667: 47.050000000000004

처음 4 개 번호는 x, y, z, 및 s의 진수 표현. IEEE 부동 소수점 표현에서 비트 2-12는 이진 지수 즉 숫자의 스케일을 나타냅니다 . (첫 번째 비트는 부호 비트이고, 가수에 대한 나머지 비트입니다 .) 표현 된 지수는 실제로 이진수에서 1023을 뺀 것입니다.

처음 4 개의 지수가 추출됩니다.

    sign|exponent
403 => 0|100 0000 0011| => 1027 - 1023 = 4
401 => 0|100 0000 0001| => 1025 - 1023 = 2
403 => 0|100 0000 0011| => 1027 - 1023 = 4
404 => 0|100 0000 0100| => 1028 - 1023 = 5

추가의 첫 세트

두 번째 숫자 ( y)는 더 작은 크기입니다. 를 얻기 위해이 두 숫자를 더할 때 두 x + y번째 숫자 ( 01) 의 마지막 2 비트 가 범위를 벗어나 계산에 포함되지 않습니다.

제 첨가 추가 x + yz같은 규모의 두 숫자를 추가한다.

추가의 두 번째 세트

여기서 x + z먼저 발생합니다. 그것들은 같은 규모이지만, 규모가 더 높은 숫자를 산출합니다.

404 => 0|100 0000 0100| => 1028 - 1023 = 5

제 첨가를 추가 x + z하고 y, 현재 3 비트로부터 삭제되는 y숫자를 추가 ( 101). 결과는 다음 부동 소수점 수이기 때문에 위쪽으로 반올림되어야합니다 4047866666666666. 첫 번째 덧셈 4047866666666667세트와 두 번째 덧셈 세트의 결과입니다. 이 오류는 총계의 출력물에 표시하기에 충분히 중요합니다.

결론적으로 IEEE 숫자에 대해 수학 연산을 수행 할 때주의하십시오. 일부 표현은 정확하지 않으며 스케일이 다르면 훨씬 더 정확하지 않습니다. 가능하면 비슷한 규모의 숫자를 더하고 빼십시오.


비늘이 다른 것이 중요한 부분입니다. 입력으로 이진수로 표시되는 정확한 값을 10 진수로 쓸 수 있지만 여전히 같은 문제가 있습니다.
Jon Skeet

@rgettman 프로그래머로서, 나는 =)당신의 16 진 프린터 헬퍼에 대한 당신 의 대답이 +1을 더 좋아합니다 ... 정말 깔끔합니다!
ADTC

44

존의 대답은 물론 맞습니다. 귀하의 경우 오류는 간단한 부동 소수점 연산을 수행하여 누적되는 오류보다 크지 않습니다. 어떤 경우에는 제로 오류가 발생하고 다른 경우에는 작은 오류가 발생하는 시나리오가 있습니다. 실제로 흥미로운 시나리오는 아닙니다. 좋은 질문은 계산 순서 변경이 작은 오류에서 (상대적으로) 큰 오류로 진행되는 시나리오가 있습니까? 대답은 분명합니다.

예를 들면 다음과 같습니다.

x1 = (a - b) + (c - d) + (e - f) + (g - h);

vs

x2 = (a + c + e + g) - (b + d + f + h);

vs

x3 = a - b + c - d + e - f + g - h;

분명히 정확한 산술에서 그들은 동일 할 것입니다. x1 및 x2 및 x3의 값이 많은 양이되도록 a, b, c, d, e, f, g, h에 대한 값을 찾으려고 시도하는 것이 재미있다. 그렇게 할 수 있는지보십시오!


대량을 어떻게 정의합니까? 우리는 1000의 순서로 이야기하고 있습니까? 100 일? 1 ???
Cruncher

3
@Cruncher : 정확한 수학적 결과와 x1 및 x2 값을 계산합니다. 실제 결과와 계산 된 결과 e1과 e2의 정확한 수학적 차이를 호출하십시오. 이제 오류 크기를 생각하는 몇 가지 방법이 있습니다. 첫 번째는 다음 중 하나 인 시나리오를 찾을 수 있습니까? | e1 / e2 | 또는 | e2 / e1 | 크다? 예를 들어, 10 배의 오차를 다른 10 배의 오차로 만들 수 있습니까? 더 흥미로운 것은 하나의 오류를 정답 크기의 상당 부분으로 만들 수 있다면입니다.
에릭 리퍼 트

1
그가 런타임에 대해 이야기하고 있다는 것을 알고 있지만,식이 컴파일 타임 (예 : constexpr)식이라면 오류가 최소화되도록 컴파일러가 똑똑합니까?
Kevin Hsu

일반적으로 @kevinhsu 아니오, 컴파일러는 그렇게 똑똑하지 않습니다. 물론 컴파일러는 원하는 경우 연산을 정확한 산술로 수행하도록 선택할 수 있지만 일반적으로 그렇지 않습니다.
Eric Lippert

8
@frozenkoi : 예, 오류는 매우 쉽게 무한 할 수 있습니다. 예를 들어, C #을 고려하십시오 double d = double.MaxValue; Console.WriteLine(d + d - d - d); Console.WriteLine(d - d + d - d);.-출력은 Infinity 다음 0입니다.
Jon Skeet

10

이것은 실제로 Java 및 Javascript 이상을 다루며 float 또는 double을 사용하는 모든 프로그래밍 언어에 영향을 줄 수 있습니다.

메모리에서 부동 소수점은 IEEE 754 라인을 따라 특수 형식을 사용합니다 (변환기는 가능한 것보다 훨씬 더 나은 설명을 제공합니다).

어쨌든, 여기에 플로트 변환기가 있습니다.

http://www.h-schmidt.net/FloatConverter/

작업 순서에 관한 것은 작업의 "정확도"입니다.

첫 번째 줄은 처음 두 값에서 29.41을 산출하여 2 ^ 4를 지수로 만듭니다.

두 번째 줄은 41.17을 산출하여 2 ^ 5를 지수로 만듭니다.

우리는 지수를 증가시켜 중요한 수치를 잃어 가고 있으며 결과는 바뀔 수 있습니다.

41.17에 대해 맨 오른쪽에있는 마지막 비트를 켜고 끄십시오. 지수의 1 / 2 ^ 23과 같이 "미미한"것이이 부동 소수점 차이를 유발하기에 충분하다는 것을 알 수 있습니다.

편집 : 중요한 인물을 기억하는 사람들에게는 이것이 해당 범주에 속합니다. 10 ^ 4 + 4999의 유효 숫자 1은 10 ^ 4가됩니다. 이 경우 유효 숫자는 훨씬 작지만 .00000000004가 첨부 된 결과를 볼 수 있습니다.


9

부동 소수점 숫자는 IEEE 754 형식을 사용하여 표현되며 가수에 대한 특정 크기의 비트를 제공합니다. 불행히도 이것은 특정 수의 '분수 빌딩 블록'을 제공하며 특정 분수 값을 정확하게 표현할 수는 없습니다.

귀하의 경우에 일어나는 것은 두 번째 경우에는 추가가 평가되는 순서로 인해 추가가 약간의 정밀한 문제에 처할 수 있다는 것입니다. 값을 계산하지는 않았지만 23.53 + 17.64는 정확하게 표현할 수 없지만 23.53 + 5.88은 정확하게 표현할 수 없습니다.

불행히도 그것은 당신이 처리해야 할 알려진 문제입니다.


6

나는 그것이 증발 순서와 관련이 있다고 생각합니다. 수학 세계에서는 합이 자연스럽게 동일하지만 A + B + C = D 대신 이진 세계에서는 동일합니다.

A + B = E
E + C = D(1)

부동 소수점 숫자가 나올 수있는 2 차 단계가 있습니다.

주문을 변경하면

A + C = F
F + B = D(2)

4
이 답변이 실제 이유를 피한다고 생각합니다. "부동 소수점 수를 얻을 수있는 2 차 단계가 있습니다." 분명히 이것은 사실이지만, 우리가 설명하고자하는 것은 왜입니까 .
Zong
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.