모두의 계산기
← 블로그로 돌아가기

재미있는 숫자 상식

IEEE 754 부동소수점 오차: 0.1+0.2가 정확히 0.3이 아닌 이유

10진 소수를 2진 부동소수점으로 저장할 때 생기는 표현 오차와 IEEE 754 binary64 구조, 반올림, 비교·누적 계산의 안전한 처리법을 설명합니다.

여러 프로그래밍 언어에서 0.1+0.2를 높은 정밀도로 출력하면 0.30000000000000004처럼 보일 수 있습니다. 덧셈 규칙이 틀린 것이 아니라 0.1과 0.2를 유한한 2진 부동소수점으로 정확히 저장할 수 없기 때문입니다. IEEE 754는 이런 근삿값의 형식, 연산 결과의 반올림, 무한대와 NaN 같은 특수값을 일관되게 다루는 표준입니다.

부동소수점 오차는 단순한 비트 절삭만이 아니라, 표현 가능한 가장 가까운 값으로 반올림하는 과정에서 생깁니다.

10진수 0.1은 왜 2진수에서 끝나지 않을까

10진법의 유한소수는 분모가 2와 5만을 소인수로 가질 때 끝납니다. 0.1=1/10은 10진법에서는 한 자리로 끝납니다. 2진법의 유한소수는 약분한 분모가 2의 거듭제곱일 때만 끝납니다. 10=2×5에는 5가 남으므로 0.1은 2진법에서 0.0001100110011…처럼 0011이 반복됩니다.

저장 공간은 유한하므로 반복되는 비트를 어느 지점에서 끝내야 합니다. 표준 연산은 요구된 형식에서 정확한 실수 결과에 가장 가까운 표현값을 고릅니다. 따라서 저장된 0.1은 수학적인 1/10과 아주 조금 다릅니다. 0.2와 0.3도 각각 근삿값이며, 근삿값을 더한 뒤 다시 반올림한 결과가 저장된 0.3과 같은 비트 패턴이 아닐 수 있습니다.

binary64의 64비트는 어떻게 나뉠까

흔히 double이라고 부르는 IEEE 754 binary64는 부호 1비트, 지수 11비트, 소수부 52비트로 구성됩니다. 정규화된 유한값은 (−1)^sign×1.fraction×2^(exponent−1023) 형태로 해석합니다. 맨 앞의 1은 정규수에서 저장하지 않고 암묵적으로 사용하므로 유효숫자 정밀도는 53비트입니다. 이는 대략 15~17자리의 10진 유효숫자를 왕복 보존할 수 있는 수준입니다.

필드binary64 비트 수역할
부호1양수와 음수
지수112의 거듭제곱 크기
소수부52유효숫자의 나머지 비트

지수 비트가 모두 0이면 0 또는 비정규수로 해석합니다. 비정규수는 0 근처의 간격을 단계적으로 이어 갑작스러운 언더플로를 줄입니다. 지수가 모두 1이고 소수부가 0이면 양·음의 무한대, 소수부가 0이 아니면 NaN입니다. +0과 −0도 서로 다른 부호 비트를 가지며 일반 비교에서는 같지만 일부 연산 결과의 부호에 영향을 줍니다.

기본 반올림은 최근접 짝수 방식

IEEE 754의 기본 반올림 방향은 정확한 값에 가장 가까운 표현값을 고르고, 정확히 중간이면 마지막 유효 자리가 짝수인 쪽을 고르는 roundTiesToEven입니다. 언제나 아래쪽 비트를 잘라 내는 방식이 아닙니다. 이 규칙은 중간값이 반복될 때 특정 방향으로 오차가 누적되는 편향을 줄입니다. 표준은 0 방향, +∞ 방향, −∞ 방향 같은 다른 반올림 방향도 정의합니다.

덧셈, 뺄셈, 곱셈, 나눗셈, 제곱근 같은 기본 연산은 무한 정밀도로 계산한 결과를 목적 형식에 한 번 반올림한 것과 같은 올바른 반올림 결과를 요구합니다. 따라서 오차가 제멋대로 발생하는 것은 아닙니다. 다만 입력 자체가 이미 근삿값이고 연산마다 유한 정밀도로 돌아오므로 긴 계산에서는 오차의 전파를 분석해야 합니다.

머신 엡실론과 ULP

binary64에서 1 다음으로 큰 표현값은 1+2⁻⁵²입니다. 이 간격 2⁻⁵²≈2.220446049250313×10⁻¹⁶을 흔히 머신 엡실론이라고 부릅니다. ULP는 특정 크기에서 인접한 표현값 사이의 간격을 뜻합니다. 부동소수점은 소수점 위치가 움직이므로 절대 간격이 일정하지 않습니다. 수가 커지면 인접값의 간격도 커지고, 2⁵³보다 큰 정수 영역에서는 모든 정수를 하나씩 표현할 수 없습니다.

큰 수에 작은 수를 더하면 사라지는 이유

지수가 크게 다른 두 수를 더하려면 작은 수의 유효숫자를 큰 수의 지수에 맞춰 이동합니다. 이동한 비트가 정밀도 밖으로 밀려나면 더한 결과가 큰 수와 같을 수 있습니다. 예를 들어 충분히 큰 binary64 값에 1을 더해도 비트 패턴이 변하지 않는 구간이 있습니다. 상대오차는 작더라도 개수나 금액처럼 1의 차이가 중요한 자료에는 문제가 됩니다.

상쇄 오차와 계산 순서

크기가 비슷한 두 근삿값을 빼면 앞쪽의 같은 유효숫자가 사라지고 입력에 있던 작은 오차가 결과에서 크게 드러날 수 있습니다. 이를 소거 또는 상쇄라고 합니다. 같은 수들을 더해도 결합법칙이 컴퓨터 계산에서 항상 성립하지 않는 이유도 중간 반올림입니다. (a+b)+c와 a+(b+c)가 서로 다른 결과를 낼 수 있으므로 합계를 낼 때 작은 절댓값부터 더하거나 보상합 알고리즘을 쓰기도 합니다.

실수 비교는 무조건 epsilon이면 될까

계산 결과를 ==로 비교하면 수학적으로 같아야 하는 값이 다르게 나올 수 있습니다. 그렇다고 모든 상황에서 고정된 10⁻⁹를 허용하면 크기가 매우 크거나 작은 값에서 기준이 부적절합니다. 보통 |a−b|≤atol+rtol×max(|a|,|b|)처럼 절대 허용오차와 상대 허용오차를 함께 씁니다. 허용치는 측정 오차와 문제의 단위를 바탕으로 정해야 하며, 머신 엡실론 하나를 만능 기준으로 삼지 않습니다.

정렬과 해시 키처럼 정확한 동일성 규칙이 필요한 곳에서는 근사 비교가 추이성을 깨뜨릴 수 있습니다. 그런 경우 값을 일정 단위로 양자화하거나 정수 단위로 바꾸고, 프로그램 전체에서 같은 동등성 규칙을 사용해야 합니다. 근사 비교는 수치해석의 종료 판정에는 유용하지만 자료구조의 동일성 정의와는 목적이 다릅니다.

돈 계산에는 왜 정수나 10진 형식을 쓸까

원 단위처럼 최소 단위가 정해진 금액은 그 단위를 정수로 저장하면 2진 소수 표현 오차를 피할 수 있습니다. 소수 금액이 필요하면 10진 부동소수점 또는 임의정밀도 decimal 형식을 사용할 수 있습니다. 다만 나눗셈처럼 결과가 끝나지 않는 연산에서는 10진 형식도 반올림 규칙이 필요합니다. 자료형만 바꾸는 것보다 통화별 최소 단위와 법정 반올림 시점을 명시하는 일이 중요합니다.

출력 자릿수와 실제 저장값

화면에 0.3이라고 보인다고 내부 값이 정확한 3/10이라는 뜻은 아닙니다. 출력 함수는 읽기 쉬운 짧은 10진 문자열을 선택할 수 있습니다. 반대로 지나치게 많은 자릿수를 출력하면 저장된 근삿값의 차이가 드러납니다. 직렬화와 역직렬화에서는 원래 비트값으로 되돌아올 만큼 충분한 유효 자릿수를 사용해야 합니다.

정수 검증 알고리즘과 비교

룬 알고리즘의 모듈로 10 체크디지트는 각 자릿수를 정수로 처리하므로 부동소수점이 필요하지 않습니다. 카드번호나 식별번호를 숫자 크기로 저장하면 선행 0이 사라지거나 정밀도 한계를 넘을 수 있으므로 문자열 또는 정수 자릿수로 다루는 편이 맞습니다. 계산 대상의 성질에 맞는 표현을 고르는 것이 오차 방지의 출발점입니다.

안전한 수치 계산 점검표

  1. 10진 유한소수가 2진에서도 정확하다고 가정하지 않습니다.
  2. 오차를 단순 절삭으로 설명하지 않고 반올림 규칙을 확인합니다.
  3. 비교 허용치는 문제의 절대·상대 규모와 단위를 반영합니다.
  4. 큰 수와 작은 수의 덧셈, 비슷한 수의 뺄셈에서 정밀도 손실을 점검합니다.
  5. 금액과 식별번호에는 목적에 맞는 정수·10진·문자열 표현을 사용합니다.

예외 상태도 계산 결과의 일부

IEEE 754는 잘못된 연산, 0으로 나누기, 오버플로, 언더플로, 부정확 결과에 대응하는 예외 상태를 정의합니다. 구현은 기본 결과와 상태 플래그를 제공할 수 있습니다. NaN은 자기 자신과도 같다고 비교되지 않으므로 누락값처럼 무심코 정렬하거나 집계하면 예상 밖 결과가 생깁니다. 무한대도 매우 큰 유한수와 같지 않습니다. 수치 프로그램은 정상 범위의 반올림 오차뿐 아니라 NaN, 무한대, 비정규수와 예외 플래그를 어떻게 처리할지 정해야 합니다.

참고 자료