> knowledge > [IEEE 754] 0.1 + 0.2 = 0.30000000000000004? 0.1 + 0.2 ≠ 0.3?

[IEEE 754] 0.1 + 0.2 = 0.30000000000000004? 0.1 + 0.2 ≠ 0.3?

Rounding Error

IEEE 754에 의해 floating point(부동소수점)을 표현하게 되는 경우 (정밀도에 따라)제한된 자릿수로 실수를 표현하게 된다. 예를들어 binary 32(single precision, 단정밀도)의 경우 가수부(significand)는 23 bit의 표현 범위를 갖는다.
그렇기 때문에 표현하려는 원래의 수가 가수부의 표현범위보다 자리수가 많다면 23 bit를 제외한 나머지는 반올림처리를 하여 잘라낼 수 밖에 없게 되는데 이때문에 부동소수점은 태생적으로 약간의 부정확성이 생길 수 밖에 없다.

binary(이진수)로 표현하는 것도 가수부의 표현범위와 연관되는 또 하나의 이유가 된다.
decimal(십진수)을 binary(이진수)로 표현하는 경우 무한히 반복되는 수가 될 수 있어 근사값 표현이 되기 때문이다.
예를 들어 십진수 0.1은 이진수로 표현하게 되면 0.00011001100110011… 이 되어 뒷부분의 0011이 무한히 반복되는 수가 된다. 이를 가수부영역에 표현하기 위해서는 반올림 처리를 해서 실제값과 가장 가까운 수로 표현할 수밖에 없게 된다.

0.1 + 0.2 = 0.30000000000000004?

대부분의 언어에서 0.1 + 0.2 = 0.30000000000000004를 표현하고 있다.
결론부터 말하자면 이런 결과는 IEEE 754표준 중 double precision floating point(배정도 부동소수점)를 사용하여 계산된 결과를 정밀도를 맞추기 위해 나머지 수를 반올림하여 표현되었기 때문이다.

Addition

반올림 오류가 발생하게되는 상황을 이해하기 위해 사칙연산 중 덧셈(addition)을 예를 들어 설명하고자 한다.

Addition rule

먼저 다음의 두 수를 더하는 부동소수점의 덧셈을 계산하는 기본적인 방법은 다음과 같다.

add_1.png

  • 지수부(exponent) q1과 q2 중 작은 값을 큰값과 같도록 맞춘다.
    이때 가수부(significand)는 변경되는 지수부에 의해 shift 처리한다.
    예를 들어 q1 < q2 라면 A의 q1값을 q2에 맞추고 이에 따라 A의 가수부 c1을 오른쪽으로 q2-q1만큼 shift한다.
  • 가수부끼리 덧셈을 계산한다.
  • 계산된 결과를 정규화(normalize)한다.
  • 가수부 bit에 맞도록 반올림 처리한다.

참고로 지수부를 동일하게 맞추고 가수부를 bit이동할때 주의할 점은 IEEE 754에서 bit표현시 정규화된 경우 가장 앞에 1이 숨겨진 bit(hidden bit)가 생략되어 있다는 것을 기억해야 한다.

계산 예시

위에서 언급했듯이 0.1+0.2의 결과에 대한 것은 binary64 형식인 배정도 부동소수점으로 계산을 해야하지만 계산의 단순함을 위해 single precision floating point(단정도 부동소수점, binary32)를 사용하기로 하고 이에따라 오차가 날 수 있는 상황인 0.1 + 0.6를 계산해 보려 한다.

참고로 설명을 위해 정밀도를 IEEE 754 표준이 아닌 수로 작게 줄여(예를 들어 정밀도를 4로 가정) 계산을 해볼 수도 있지만 어느정도의 표준과 동일한 상황을 확인해 보기위해 binary32를 선택하였다.
그러기 위해선 0.1 + 0.2가 아닌 다른 결과 값이 필요하기때문에 0.70000005가 발생하는 0.1 + 0.6를 선택하였다.

0.1 + 0.6 ≠ 0.7

0.1 + 0.6 = 0.7이 우리가 원하는 계산결과지만 부동소수점에서는 왜 0.7이 되지 않는지를 확인해볼 필요가 있다.
덧셈 규칙에 따라 계산하기 전에 먼저 IEEE 754에 따른 binary32 형식으로 각 수를 표현해 보면 다음과 같다.

add_2add_3.png

1. 지수부 맞추기

이제 계산 규칙에 따라 먼저 지수부의 숫자를 같도록 맞춰보면 0.6의 지수부는 -1, 0.1의 지수부는 -4로 0.6의 지수부가 더 크다. 그러므로 0.1의 지수부를 -1가 되도록 맞춘 후 그에 따른 가수부를 변경하면 다음과 같다.

add_4.png

지수부 -4를 -1로 변경하고 그에 따라 소수점을 이동시켰다.

2. 가수부 덧셈 계산

이제 두 수의 가수부를 더해보면 다음과 같다.

add_5.png

이를 정리하여 binary 32 형식으로 표현하면 다음과 같다.

add_6.png

3. 계산된 결과를 정규화

계산될 결과가 이미 1.으로 시작하고 있어 이미 정규화가 되어있기 때문에 정규화 작업은 하지 않아도 된다.
만약 계산 결과가 다음과 같았다면 정규화를 진행해야한다.

add_7.png

4. 가수부 정밀도 맞추기

binary 32(단정도 부동소수점)의 가수부 bit 수는 23개이다. 현재 계산된 결과는 26개의 bit로 표현되어야 하므로 뒤의 3 bit를 반올림하여 버려야한다.
(사실 소수점 앞까지 하면 27 bit지만 IEEE 754 표준에 의해 가장 앞의 1은 숨겨진 bit로 처리하기 때문에 실제 bit표현시에는 bit에 담을 필요가 없다.)

add_8.png

정밀도를 맞추기 위해 반올림 처리를 해야하는데 IEEE 754에서는 Round to nearest, tites to even 을 기본으로 하고 있다.
반올림 할 값이 101이므로 이는 올림처리가 되어야한다. 그러므로 반올림 처리한 결과는 다음과 같다.

add_9.png

이를 정리하여 binary 32 형식으로 표현하면 다음과 같다.

add_10.png

덧셈 계산은 끝났다. 위의 결과를 십진수로 표현했을때 어떻게 되는지 확인해 보면 0.7이 아닌 0.70000005가 된다.

마찬가지 이유로 인해 글 제목과 같이 0.1 + 0.2를 double precision floating point(배정도 부동소수점)으로 계산해보면 0.3이 아닌 0.30000000000000004이 된다.
참고로 0.1 + 0.2를 single precision floating point(단정도 부동소수점)으로 계산한다면 0.3이 나온다. 이는 정밀도에 따른 차이로 단정도에서 오차가 발생하지 않는다고 해서 배정도에도 오차가 발생하지 않는다는 것을 뜻한다.
반대로 위의 예시였던 0.1 + 0.6은 single precision floating point에서는 0.70000005가 되지만 double precision floating point에서는 0.7로 정확하게 계산된다.

결론

부동소수점 연산시 오차가 발생하는 이유에 대해서는 서두에 요약하여 언급을 했듯이 반올림으로 인해 오차가 발생하기 때문이다.
참고로  (완벽하진 않지만) 최대한 정확한 결과를 얻기위해서는 몇가지 방법이 있다.

  • 이진수 형태(base가 2인)의 부동소수점보다는 십진수(base가 10인) 부동소수점을 사용한다.
    이렇게 하면 정밀도 자리수에 따른 반올림 오류는 여전히 존재하겠지만 적어도 십진수를 이진수로 변환할때 무한한 수가 되는 상황은 해결할 수 있다.
    예를들어 Java의 경우 float, double보다는 Decimal, BigDecimal 을 사용하면 된다.
  • 정밀도가 보다 높은 부동소수점을 사용한다.
    binary 32를 사용한다면 binary 64를 decimal 64를 사용한다면 decimal 128을 사용하면 정밀도가 높아지므로 연산에 의한 오차가 작아지게 된다.
  • 가능하면 부동소수점 연산을 사용하지 않는다.
    예를 들어 1.0 + 0.1 + 7을 계산해야한다고 하면 이를 모두 정수형태로 변형하기 위해 모든 수에 10을 곱하여 10 + 1 + 70으로 계산하고 계산된 결과를 다시 10으로 나누어 실수형태로 표현한다.

[참고자료]

Floating Point MathFloating Point Math

NUM04-J Do not use floating-point numbers if precise computation is required

카테고리:knowledge
  1. 댓글이 없습니다.
  1. No trackbacks yet.

댓글 남기기