이 장을 읽기 전에
SIMD에서 수치 연산을 대량으로 처리하는 벡터 연산을 다뤘다. 이 챕터는 그 수치 연산이 다루는 실수 자체가 컴퓨터 안에서 어떻게 표현되는지, 그리고 왜 그 표현이 수학적으로 정확한 실수와 다를 수밖에 없는지를 다룬다. 이 챕터는 SIMD·성능보다는 정확성·디버깅 관점에서 실수 연산을 이해하는 데 필요한 기초다.
IEEE 754: 부호·지수·가수로 실수를 근사하기
정수는 유한한 비트로 정확히 표현할 수 있지만, 실수는 무한히 많은 소수점 아래 자리를 가질 수 있어 유한한 비트로는 근본적으로 근사할 수밖에 없다. IEEE 754는 이 근사를 표준화한 규격으로, 실수를 부호(Sign), 지수(Exponent), 가수(Mantissa/Significand) 세 부분으로 나눠 표현한다. 32비트 단정밀도(float)는 부호 1비트, 지수 8비트, 가수 23비트로 구성되고, 64비트 배정밀도(double)는 부호 1비트, 지수 11비트, 가수 52비트로 구성된다. 값은 대략 (-1)^부호 × 1.가수 × 2^(지수 - 편향값) 형태로 계산되며, 이는 과학적 표기법(예: 1.234 × 10^5)을 2진수 버전으로 옮긴 것과 같은 발상이다.
| |
0.1 + 0.2가 정확히 0.3이 되지 않는 이유
문제는 10진수로 유한한 소수(0.1, 0.2)조차 2진수로는 무한소수가 되는 경우가 많다는 데 있다. 0.1을 2진수로 바꾸면 0.0001100110011...처럼 0011이 무한 반복되는데, IEEE 754는 유한한 비트(가수 23비트 또는 52비트)만 쓸 수 있으므로 이 무한소수를 어딘가에서 잘라 근사값으로 저장한다. 0.1과 0.2는 각각 아주 근소하게 다른 근사 오차를 갖고 저장되고, 이 둘을 더하면 오차가 누적돼 0.3을 표현하는 이진 근사값과 정확히 일치하지 않는 값이 나온다. 이는 버그가 아니라 2진 부동소수점으로 10진 소수를 정확히 표현할 수 없다는 근본적 한계다.
| |
gcc -std=c11 float_demo.c -o float_demo && ./float_demo로 컴파일·실행하면 sum과 0.3이 서로 다른 비트 패턴으로 저장돼 있어 == 비교가 false를 반환하는 것을 직접 확인할 수 있다. %.20f로 소수점 아래 20자리까지 출력하면, 0.1 + 0.2의 실제 저장값이 수학적인 0.3보다 아주 미세하게 큰 값이라는 것이 드러난다.
부동소수점 비교에는 오차 허용(epsilon)이 필요하다
위 예시가 보여주듯, 부동소수점 값 두 개를 ==로 직접 비교하는 것은 대부분의 상황에서 안전하지 않다. 대신 두 값의 차이가 아주 작은 허용 오차(epsilon) 이내인지를 확인하는 방식을 쓴다.
| |
epsilon 값은 다루는 수치의 크기와 요구 정밀도에 따라 달라져야 한다 — 아주 큰 수를 비교할 때 1e-9처럼 작은 고정 epsilon을 그대로 쓰면, 그 수 자체의 표현 오차가 이미 epsilon보다 커서 여전히 오탐이 날 수 있다(구현 정의: 실무에서는 상대 오차 방식이나 언어별 부동소수점 비교 유틸리티를 함께 검토해야 한다).
언제 float/double을 아예 피해야 하는가
epsilon 비교로도 해결되지 않는 상황이 있다. 금융 계산(통화 금액, 이자 계산)이 대표적이다. 반올림 오차가 거래마다 누적되면 “1원 단위까지 정확히 맞아야 한다"는 요구사항 자체를 만족시킬 수 없으므로, 대부분의 결제·회계 시스템은 부동소수점 대신 정수(예: 원 단위 대신 전 단위로 저장) 또는 언어가 제공하는 고정소수점·decimal 타입(Python의 decimal.Decimal, Java의 BigDecimal 등)을 쓴다. 이런 타입은 10진 소수를 10진 그대로 저장해 0.1 + 0.2류의 근사 오차 자체가 발생하지 않는다.
특수값 비교도 별도로 주의해야 한다. IEEE 754는 “정의되지 않은 결과"를 나타내는 NaN(Not a Number)을 규정하는데, 표준상 NaN은 자기 자신과도 같지 않다고 정의되어 있어 x == NaN은 x가 무엇이든 항상 false를 반환한다(심지어 NaN == NaN도 false다). 어떤 값이 NaN인지 확인하려면 == 비교가 아니라 isnan() 같은 전용 함수를 써야 한다. 마찬가지로 Infinity도 유한한 수와의 산술 연산에서 흔히 등장하므로(예: 0으로 나누기), 사용자 입력이나 계산 중간값이 NaN·Infinity가 될 수 있는 코드라면 최종 결과를 쓰기 전에 isnan()/isinf()로 검증하는 습관이 필요하다.
비교: 정수 표현 vs 부동소수점 표현
| 특성 | 정수(int) | 부동소수점(float/double) |
|---|---|---|
| 표현 방식 | 값을 그대로 2진수로 저장 | 부호·지수·가수로 근사 |
| 정확도 | 표현 범위 내에서 항상 정확 | 대부분의 10진 소수를 근사값으로만 표현 |
| 덧셈 결합법칙 | 항상 성립 | 반올림 오차로 인해 항상 성립하지는 않음 |
동등 비교(==) | 안전 | 대부분 위험, epsilon 비교 권장 |
흔한 오개념
“0.1 + 0.2 != 0.3은 특정 언어나 컴파일러의 버그다” — 이는 C뿐 아니라 IEEE 754를 따르는 거의 모든 언어(Python, Java, JavaScript 등)에서 똑같이 재현된다. 언어의 결함이 아니라, 10진 소수를 유한한 2진 비트로 정확히 표현할 수 없다는 부동소수점 표준 자체의 근본 한계다.
“double을 쓰면 float보다 항상 정확해서 오차 문제가 사라진다” — double은 가수 비트가 더 많아 근사 오차가 작아질 뿐, 근사 자체가 사라지지는 않는다. 0.1 + 0.2 != 0.3은 double에서도 여전히 재현된다(위 코드가 실제로 double을 쓴 예시다). 정밀도를 높이는 것과 오차를 완전히 없애는 것은 다른 문제다.
다른 개념과의 연결
SIMD에서 다룬 벡터 연산은 부동소수점 배열에 자주 쓰이는데, 반복 순서에 따라 반올림 오차가 누적되는 순서가 달라질 수 있어 SIMD로 재정렬된 연산이 스칼라 연산과 미세하게 다른 결과를 낼 수 있다는 점이 여기서 이어진다. 다음 챕터에서는 명령어와 데이터가 같은 메모리 공간에 저장되는 폰 노이만 구조를 다룬다.
평가 기준
이 챕터를 읽은 후에는 다음을 할 수 있어야 한다. IEEE 754가 부호·지수·가수로 실수를 근사하는 방식을 설명할 수 있다. 0.1 + 0.2 != 0.3이 왜 버그가 아니라 2진 부동소수점의 근본적 한계인지 설명할 수 있다. 부동소수점 비교에 epsilon이 필요한 이유와, epsilon 선택 시 주의할 점을 설명할 수 있다. 금융 계산처럼 반올림 오차를 허용할 수 없는 경우 왜 float/double 대신 decimal·고정소수점을 써야 하는지, NaN/Infinity를 == 대신 isnan()/isinf()로 검사해야 하는 이유를 설명할 수 있다.
참고 자료
IEEE Standard for Floating-Point Arithmetic (IEEE 754-2019), IEEE Computer Society.
- Wikipedia: IEEE 754 — IEEE 754 형식의 비트 구성과 특수값(NaN, Infinity 등) 개요
- floating-point-gui.de: What Every Programmer Should Know About Floating-Point Arithmetic — 부동소수점 비교·오차 허용에 대한 실무 가이드
![Featured image of post [Computer Terms] 부동소수점 표현 (IEEE 754 Floating Point)](/post/computerterms/floating-point-representation/wordcloud_hu_f417d8bf00afbe76.webp)
![[Computer Terms] 캐시 계층: L1/L2/L3 (Cache Hierarchy)](/post/computerterms/cache-hierarchy/wordcloud_hu_b1be6121ff2b20e2.webp)
![[Computer Terms] SIMD (Single Instruction, Multiple Data)](/post/computerterms/simd/wordcloud_hu_668c76ddc67ba142.webp)
![[Computer Terms] 부동소수점 표현 (IEEE 754 Floating Point)](/post/computerterms/floating-point-representation/wordcloud_hu_13ea1b6f4f92b338.webp)
![[Computer Terms] 폰 노이만 구조 (Von Neumann Architecture)](/post/computerterms/von-neumann-architecture/wordcloud_hu_9bad09a5a1cb7672.webp)
![[Computer Terms] 트랜잭션 격리 수준 (Isolation Levels)](/post/computerterms/transaction-isolation-levels/wordcloud_hu_bdc74f0940a9206a.webp)
![[Computer Terms] 레지스터와 명령어 집합 구조 (Register, ISA)](/post/computerterms/registers-and-isa/wordcloud_hu_ebe5309b102936ea.webp)
![[Computer Terms] 멀티레벨 캐싱 (Multi-Level Caching)](/post/computerterms/multilevel-caching/wordcloud_hu_94602a4cbdd2bcfb.webp)
![[Computer Terms] 인터럽트와 시스템 콜 (Interrupt, System Call)](/post/computerterms/interrupts-and-system-calls/wordcloud_hu_97a769dba7715f4f.webp)