1. 개요

- IEEE-754는 전기전자공학자협회(Institute of Electrical and Electronics Engineers, IEEE)에서 1985년 제정한 부동소수점의 수식 계산에 관한 표준이다. 그 전까지는 여러 하드웨어 장치에서 각자 다른 방식으로 부동소수점 연산을 구현해 각 장치가 각각 다른 문제점을 갖고 있어 이를 사용하는 것이 안정적이고 이식 가능하지 않았으나, 이 표준의 제정 이후 많은 장치들이 이 표준을 따르게 되어 이 이후 부동소수점 연산에 관해 많은 문제들이 해결되었다.

- IEEE-754 표준은 하드웨어가 반드시 32비트 단정밀도(single-precision)를 구현하도록 되어 있으며, 그 이상은 선택적으로 구현하도록 돼있다. 한편 대부분의 프로그래밍 언어는 64비트 배정밀도(double-precision) 자료형을 지원한다. 대표적으로 C언어는 float 자료형으로 32비트 단정밀도를, double 자료형으로 64비트 배정밀도를 지원한다. 파이썬의 경우 32비트 단정밀도를 지원하지 않으며, float 자료형으로써 64비트 배정밀도를 지원한다. 자바스크립트의 경우 C언어 등에 있는 정수형 자료형이 없고 모든 숫자를 기본적으로 Number형이라는 64비트 배정밀도로 표현한다.

2. IEEE-754의 부동소수점 표현의 구조

- IEEE-754 부동소수점 표현에서는 모든 숫자를 (1)부호 (2)밑이 2일 때의 지수 (3)수를 지수부로 나누고 남은 가수 세 부분으로 나눠 각각의 정보를 이진수로 표현한다. 먼저, 그 숫자를 2의 지수승으로 나눈 값이 2보다 작고 1보다 크거나 같아지게 하는 지수로 그 숫자를 나눈 후, 그렇게 얻은 지수와 가수를 각각 이진수로 표현한다. 즉, IEEE-754 부동소수점 표현의 핵심은 숫자를 먼저 1.xx \(\times 2^{\mathsf{xx}}\) 꼴로 표현하고 여기서 부동소수점 표현을 이루는 부호, 지수, 가수를 채울 정보를 얻는 것이다. (따라서, 가수의 값은 항상 1보다 크거나 같고 2보다 작다. 단, 원래 수가 0이었을 때에는 가수의 값도 0이 된다.)

  • 부호: 최상위비트로 표현된다. 0이면 양수, 1이면 음수다.

  • 지수: 단정밀도는 8개 비트, 배정밀도는 11개 비트로 표현되며, 최상위비트 바로 다음 영역을 차지한다. 음수 지수 표현을 위해 특별히 부호 비트가 따로 할당되거나 하지는 않고, 가장 작은 음수 지수를 1으로 정한 후 이 값부터 차례대로 증가하는 것으로 표현된다. 이에 따라 단정밀도의 경우 위에서 지수 0을 얻었다면 이 값은 숫자 127의 이진 표현인 01111111으로 표현되며, 1 이상의 지수를 얻었다면 이는 01111111보다 더 커진다. 이를 두고 ‘지수가 편향됐다’라고 봐, IEEE-754 부동소수점 표현의 지수부를 편향지수(biased exponent)라 한다.

    • 지수부가 0인 경우: 숫자가 0이 아닐 땐 지수부의 값은 항상 1 이상의 값을 가지며, IEEE-754 부동소수점 표현에서 지수부가 0인 경우는 그 수가 0을 표현한 것으로 본다.

    • 지수부의 모든 자릿수가 1인 경우: 가수부의 값이 모두 0이면 부호에 따라 음 또는 양의 무한대를 의미하는 것으로 정의된다. 단, 가수부의 값 중에 0이 아닌 값이 있으면 부호에 상관 없이 NaN 값을 의미하는 것으로 정의된다.

  • 가수: 부호 비트, 지수 비트 이후 나머지 모든 비트로 표현된다. 가수부의 숫자가 1.xx로 나왔다 할 때, 1은 제외하고 그 밑 소수 부분만 표현된다.

3. IEEE-754 부동소수점의 rounding 규칙

- IEEE-754 부동소수점 방식으로 표현된 수를 보다 적은 비트수로 변환하거나(예를 들면, 배정밀도로 표현된 수를 단정밀도로 변환하는 경우 등) 수끼리의 연산을 통해 더 많은 비트수로 표현된 수를 더 적은 비트수로 줄여 표현할 경우에는 어떤 규칙에 의해 정보를 버려야 하는지가 중요한 문제가 된다. 이에 대해 IEEE-754에서 규정된 원칙을 rounding rule이라 한다.

- IEEE-754에서 규정된 rounding rule은 다음 5가지가 있다.

  • round to nearest, ties to even: 그 자릿수에서 올렸을 때 그 윗자리 수와 버렸을 때 그 윗자리 수를 비교해 그 수와 가장 가까운 수로 올리거나 버린다. 단, 올리거나 버려야 하는 값의 크기가 양쪽 모두 동등하다면 그 윗자리 수가 짝수가 되도록 올리거나 버린다. 흔히 Banker’s rounding 또는 Gaussian rounding이라고도 한다.

    • 예를 들어 10진수 12.34를 소수점 첫째자리에서 이 방식으로 rounding 한다 하면, 버렸을 때(=12)가 올렸을 때(=13)보다 가까우므로 rounding한 결과값은 12가 된다.

    • 한편, 10진수 12.5를 소수점 첫째자리에서 이 방식으로 rounding을 한다 하면, 버렸을 때가 12로 짝수이고 올렸을 때가 13으로 홀수이므로 rounding한 결과값은 12가 된다.

    • 2진수에서 어떻게 이 룰을 적용할 수 있는지는 이렇게 생각하면 된다. 예를 들어, 소수점 셋째자리까지 있는 수를 첫째 자리에서 rounding을 한다고 생각하자. 이 상황에서 10진수 숫자는 0.000에서 0.999까지 변하고 중간값은 0.500인데, 같은 관점에서 볼 때 2진수 숫자는 0.000에서 0.111까지 변한다. 따라서 2진수 숫자의 소수점 첫째자리 중간값은 0.100임을 알 수 있다.

    • 2진수 1.011을 소수점 첫째자리에서 이 방식으로 rounding을 한다 하면, 버렸을 때(=1)가 올렸을 때(=10)보다 가깝다. 버리는 건 0.011만 버리면 되지만, 올리려면 추가로 0.101을 더해야 하기 때문이다. 따라서 이를 rounding한 결과값은 0이 된다.

    • 2진수 1.100을 소수점 첫째자리에서 이 방식으로 rounding을 한다 하면, 버렸을 때가 1으로 홀수이고 올렸을 때가 10로 짝수이므로(2진수에서 짝수는 끝자리 수가 0인 수를 말한다) rounding한 결과값은 10이 된다.

    • 짝수로 맞추었을 때의 이득: 이런 관점에서 생각할 수 있다. 예를 들어 수많은 숫자 집합을 rounding 처리 하도록 입력이 주어졌을 때 만약 올리거나 버려야 하는 값이 같은 경우 항상 올리거나 항상 내리는 식으로 처리하면, 결과적으로 전체 값의 평균이 ‘항상 올려라’라는 원칙대로 처리하면 올라가고 ‘항상 내려라’라는 원칙대로 처리하면 내려가는 위험이 있다. 이때 만약 ‘짝수로 맞춰라’라는 식으로 처리하면 평균적으로 절반은 올리고 절반은 내리는 식으로 처리할 것이므로 앞의 방식에 비하면 전체 값의 평균이 원본과 크게 달라지지 않을 확률이 있다. 이러한 이점으로 여러 분야에서 기본 rounding 방식으로 널리 선호되며, IEEE-754의 기본 rounding rule로도 지정돼 있다. 파이썬, 자바스크립트 등 여러 언어에서 역시 기본 rounding rule로 지정돼 있다. (한편 각 언어에서는 내부 모듈에서 제공하는 함수를 통해 기본 rounding rule 외 다른 rounding rule을 사용할 수 있다.)

  • round to nearest, ties to away: 그 자릿수에서 올렸을 때 그 윗자리 수와 버렸을 때 그 윗자리 수를 비교해 그 수와 가장 가까운 수로 올리거나 버린다. 단, 올리거나 버려야 하는 값의 크기가 양쪽 모두 동등하다면 절대값이 커지는 방향으로 올린다. 한국어로 흔히 ‘사사오입’이라 불리는 방식이다.

  • round toward 0: 0에 가깝게 rounding을 한다. (즉, 절대값이 작아지는 방향으로 버린다.)

  • round toward +\(\infty\): 양의 무한대에 가깝게 rounding을 한다. (즉, 항상 올린다. 음수일 땐 절대값이 작아진다.)

  • round toward -\(\infty\): 음의 무한대에 가깝게 rounding을 한다. (즉, 항상 버린다. 음수일 땐 절대값이 커진다.)

4. IEEE-754 부동소수점 표현의 한계로 인한 계산 결과 오류들

1) 오류의 원인

- IEEE-754 부동소수점 표현은 모든 수를 유한한 개수의 비트로 취급하는데, 실제 세상의 숫자 중에는 유한 개의 숫자로 절대 표현할 수 없는 숫자들이 많다는 점에서 IEEE-754 부동소수점 표현 방식에는 한계가 있다.

- 대표적으로 10진수 소수에 관한 문제가 있다. 10진수 정수의 경우 2진수로 정확한 값 표현이 가능하나, 10진수 소수를 2진수로 표현하려 하는 경우 10진수일 때는 유한소수더라도 2진수 유한소수로는 표현할 수 없는 경우가 있다. 예를 들어 10진수 0.1은 2진수로 0.000110011001...과 같이 1100이 무한히 반복되는 무한소수다. 그런데 IEEE-754는 항상 수를 유한한 개수의 비트로만 표현하므로, 이와 같은 10진수 소수는 IEEE-754 부동소수점 표현 방식으로 절대로 정확히 표현할 수 없다. 10진수 0.1을 32비트 단정밀도로 표현할 경우 가수부가 10011001100110011001101가 되는데, 이 값을 10진수로 변환하면 0.100000001490116119384765625이 된다. 물론 이 값은 0.1에 대단히 가까운 값이지만, 경우에 따라서는 절대 무시할 수 없을 정도로 큰 차이기도 하다.

2) 오류의 구체적 사례

- 이와 같은 원리로 여러 프로그래밍 언어에서는 소수끼리의 연산 결과가 실제와 차이가 나는 경우가 많다. 이러한 문제는 구체적으로 다음과 같은 문제를 발생시킨다.

  • 서로 같은 크기의 숫자임에도 불구하고 오차로 인해 같은 숫자로 인식하지 못하는 문제. 예를 들어, 파이썬에서 0.1 + 0.1 + 0.10.3이 아니다. 다음 코드를 실행시켜 보면, 결과로 False를 출력한다.
1
print(0.1 + 0.1 + 0.1 == 0.3)
  • 특정 숫자를 0으로 나누는 것임에도 에러가 발생하지 않는 문제

  • 지수부가 너무 커지거나 너무 작아져 무한대로 표현하는 문제

  • 결합법칙/분배법칙이 성립하지 않는 문제

  • 로그나 탄젠트 같은 함수에 정의역 밖의 숫자(0이나 \(\pi \over 2\) 등)를 입력으로 넣어도 에러가 발생하지 않는 문제

3) 오류 대처 방안

- 이러한 문제 때문에 소수를 보다 정확하게 처리하는 특별한 방식이 여럿 논의된다. 다음은 몇가지 대표적인 방식이다.

  • 수동으로 rounding 처리를 한다. (예를 들어, 그 프로그램에서 사용하는 소수가 최대 소수점 7번째 자리 이상으로 정밀할 필요가 없다면, 소수끼리 연산 또는 대소비교를 할 때마다 소수점 8번째 자리 이후를 반올림 한 후 연산 또는 대소비교를 하도록 처리한다.)

  • 크기가 동일해 보이는 두 수의 동일 여부 판별을 위해, 상대오차 및 ‘아주 작은 수(epsilon)’의 개념을 사용한다. 즉, ‘두 수의 상대오차가 입실론보다 작다면 두 수는 같은 수’라고 처리한다.

  • 소수의 크기 비교를 위해 특별히 정의된 함수를 사용하거나(예를 들어, 파이썬의 math.isclose() 함수 등) 소수의 정확한 값 표현을 위한 특수한 객체를 사용한다. (예를 들어, 파이썬에서는 decial 모듈의 Decimal 객체를 사용한다. 단, 이 Decimal 객체를 사용한다 하더라도 정확한 값 표현이 불가능할 때는 반드시 있다.)