IEEE 754 부동소수점 비트 패턴 시각화 도구

10진수를 IEEE 754 단정밀도(32비트) 또는 배정밀도(64비트) 비트 패턴으로 분해하거나, 비트 패턴을 다시 10진수로 디코딩합니다. 부호·지수부·가수부를 색상으로 구분해 표시하며, NaN·무한대·비정규화수도 지원합니다.

고전적인 예: 0.1 + 0.2가 왜 0.3이 되지 않는가

JavaScript에서 `0.1 + 0.2`를 계산하면 `0.3`이 아니라 `0.30000000000000004`가 됩니다. 배정밀도 비트 패턴을 살펴보면 그 이유를 알 수 있습니다.

배정밀도(64비트) 16진수 배정밀도(64비트) 2진수
0.1 0x3FB999999999999A 0011111110111001100110011001100110011001100110011001100110011010
0.2 0x3FC999999999999A 0011111111001001100110011001100110011001100110011001100110011010
0.3 0x3FD3333333333333 0011111111010011001100110011001100110011001100110011001100110011
0.1 + 0.2 0x3FD3333333333334 0011111111010011001100110011001100110011001100110011001100110100

0.1도 0.2도 0.3도 2진수로는 무한히 반복되는 소수가 되기 때문에, 52비트의 유한한 가수부에는 정확히 들어맞지 않아 각각 반올림됩니다. 0.1과 0.2의 반올림 오차 방향이 우연히 서로 상쇄되지 않기 때문에, 두 값을 더한 결과는 0.3을 정확히 나타내는 비트 패턴(0x3FD3333333333334)과 1비트만 다른 값이 됩니다. 위의 변환 도구에 이 값들을 직접 입력해 마지막 비트의 차이를 확인해 보세요.

사용 팁

  • 0.1·0.2·0.3을 배정밀도로 각각 비트로 변환한 뒤, 0.1+0.2의 결과(0.30000000000000004)와 비교해 보면 마지막 비트만 다르다는 것을 눈으로 확인할 수 있습니다.
  • 10진수 입력란에 "-0"을 입력하면 부호 비트만 1이 된 비트 패턴("+0"과는 다름)을 얻을 수 있습니다. 수치상으로는 0 === -0이지만 비트 표현은 서로 다릅니다.
  • 지수부가 모두 0일 때, 가수부가 0이면 0을 의미하고 0이 아니면 비정규화수(정밀도를 희생해 0에 매우 가까운 값을 표현하는 특수한 방식)를 의미합니다.
  • 0.1 같은 동일한 값에 대해 단정밀도(32비트)와 배정밀도(64비트)를 전환해 보면, 단정밀도 쪽의 반올림 오차가 더 크다는 것을 확인할 수 있습니다.
  • 비트 패턴을 10진수로 역변환하려면 `0x` 접두사가 붙은 16진수 문자열이나, 선택한 정밀도와 같은 자릿수의 2진수 문자열(`0b` 접두사는 생략 가능)을 입력하면 됩니다.

자주 묻는 질문

0.1·0.2·0.3은 모두 2진수로는 무한히 반복되는 순환소수가 되기 때문에, 컴퓨터는 유한한 가수부(배정밀도라면 52비트)에 담기 위해 각각을 반올림합니다. 0.1과 0.2의 반올림 오차 방향이 우연히 서로 상쇄되지 않기 때문에, 두 값을 더한 결과는 0.3을 정확히 나타내는 비트 패턴과 미세하게 다른 값(0.30000000000000004)이 됩니다. 이는 특정 언어의 버그가 아니라 IEEE 754를 준수하는 모든 언어에서 공통적으로 발생하는 현상입니다.

단정밀도(float, 32비트)는 부호 1비트·지수부 8비트·가수부 23비트로 구성되고, 배정밀도(double, 64비트)는 부호 1비트·지수부 11비트·가수부 52비트로 구성됩니다. 가수부 비트 수가 많을수록 유효 자릿수(정밀도)가 높아지고, 지수부 비트 수가 많을수록 표현 가능한 값의 범위가 넓어집니다. 대부분의 프로그래밍 언어는 부동소수점 타입의 기본값으로 배정밀도를 사용합니다.

지수부가 모두 0인 비트 패턴 중에서 가수부가 0이 아닌 경우를 가리킵니다. 일반적인(정규화된) 부동소수점 수는 가수부 맨 앞에 암묵적인 1이 붙지만, 비정규화수는 이를 생략하고 대신 지수를 최솟값에 고정함으로써 일반적으로 표현 가능한 최솟값보다도 더 0에 가까운 값을 표현할 수 있습니다. 다만 유효 자릿수는 희생됩니다.

IEEE 754에는 부호 비트가 독립적으로 존재하므로, 지수부와 가수부가 모두 0이더라도 부호 비트가 0이면 "+0", 1이면 "-0"이라는 서로 다른 두 비트 패턴이 만들어집니다. 수치 비교에서는 0 === -0으로 같다고 판정되지만, 1/0은 +Infinity가 되고 1/-0은 -Infinity가 되는 등 연산 결과에서 차이가 드러나는 경우가 있습니다.

둘 다 지수부가 모두 1인 비트 패턴으로 표현됩니다. 가수부가 모두 0이면 무한대(부호 비트로 +Infinity/-Infinity를 구분)이고, 가수부가 0이 아니면 NaN(Not a Number, 비수)입니다. 0으로 나누기나 ∞-∞처럼 수학적으로 정의되지 않은 연산의 결과로 생성됩니다.
ツールくん

여담 ― IEEE 754가 "공통 언어"가 되기까지

1985년 이전에는 컴퓨터 제조사마다 각자 독자적인 부동소수점 표현 방식을 사용했습니다. IBM·DEC·Cray 등은 "0.1"이라는 같은 값을 계산하더라도 반올림 방식과 표현 가능한 범위가 미묘하게 달라, 어떤 기계에서 작성한 수치 계산 프로그램을 다른 기계로 옮기면 계산 결과가 조금씩 어긋나는 일이 흔했습니다. 이 혼란을 해소하기 위해, 인텔 8087 수치 연산 코프로세서 설계에도 참여했던 캘리포니아 대학교 버클리의 William Kahan을 중심으로 한 작업 그룹이 업계 전반의 표준 제정을 주도했고, 그 결과 1985년에 IEEE 754가 발표되었습니다.

IEEE 754의 핵심 아이디어는 유한한 비트 수(32비트 또는 64비트)로 무한히 존재하는 실수를 근사적으로 표현하기 위해, 부호·지수부·가수부라는 세 가지 역할로 나눈 것입니다. 지수부는 값의 규모(크기)를 담당하고 가수부는 유효 자릿수(정밀도)를 담당함으로써, 매우 작은 값부터 매우 큰 값까지 제한된 비트 수로도 유연하게 표현할 수 있는 구조입니다. 지수부를 저장할 때 바이어스(단정밀도는 127, 배정밀도는 1023)를 더하는 것은, 부호 있는 정수용 2의 보수 같은 복잡한 부호 처리 없이도 비트열을 단순 비교하는 것만으로 크고 작음을 판정할 수 있도록 하기 위한 방식입니다.

2진수로 "깔끔하게" 표현할 수 있는 소수는 분모가 2의 거듭제곱인 분수(1/2, 1/4, 3/8 등)로 한정됩니다. 10진수 0.1은 분수로 쓰면 1/10인데, 10은 2의 거듭제곱이 아니므로 2진수로는 0.0001100110011...처럼 무한히 반복되는 순환소수가 됩니다. 유한한 자릿수의 가수부에 담으려면 어딘가에서 잘라내어 반올림할 수밖에 없으며, 이것이 "0.1을 컴퓨터로 정확히 표현할 수 없는" 근본적인 이유입니다. 정확한 10진 연산이 필요한 회계 시스템 등이 부동소수점 대신 정수 연산이나 전용 10진수 타입(BCD, decimal 타입)을 사용하는 것도 바로 이 때문입니다.

IEEE 754는 이후 2008년과 2019년에 개정되어 반정밀도(16비트)와 4배정밀도(128비트), 10진 부동소수점 규격 등이 추가되었지만, 근간이 되는 부호·지수부·가수부의 3분할 구조와 반올림 방식은 1985년 초판 이후로 변하지 않았습니다. 거의 모든 프로그래밍 언어의 float/double 타입이 이 규격을 따르기 때문에, "왜 0.1+0.2가 0.3이 되지 않는가"라는 의문은 언어를 불문하고 공통적으로 발생하는 현상입니다.