0.1 더하기 0.2가 0.3이 아닌 이유

거의 모든 프로그래밍 언어에서 0.1 더하기 0.2를 계산하면 0.3이 아닌 0.30000000000000004 같은 값이 나온다. 처음 이걸 본 개발자는 보통 언어 버그라고 생각한다. 그러나 이건 버그가 아니라 컴퓨터가 실수를 표현하는 방식의 구조적 한계다. 이 한계를 체계적으로 정리한 데이비드 골드버그(David Goldberg)의 “모든 컴퓨터과학자가 부동소수점 연산에 대해 알아야 할 것”은 1991년에 발표된 이래 30년 넘게 이 분야 표준 참고문헌으로 인용된다.
컴퓨터는 모든 숫자를 이진법으로 저장한다. 정수는 이진법으로 정확히 표현할 수 있다. 그러나 0.1을 이진법으로 표현하면 0.0001100110011001100… 처럼 무한 반복되는 소수가 된다. 10진법에서 1을 3으로 나눈 0.333… 이 무한히 반복되는 것과 같은 이치다. 컴퓨터는 이 무한 소수를 유한한 비트 안에 저장해야 하므로 어딘가에서 잘라낸다. 잘려나간 부분이 오차로 남는다.
IEEE 754가 표준이 되기 전의 혼란
1985년 IEEE 754 표준이 제정되기 전까지 컴퓨터마다 부동소수점 표현 방식이 달랐다. 같은 계산이 IBM 컴퓨터와 DEC 컴퓨터에서 다른 결과를 냈다. 과학 계산이 컴퓨터에 의존하기 시작하면서 이 비호환성은 큰 문제가 되었다. UC 버클리의 윌리엄 카한(William Kahan)이 주도해 만든 IEEE 754 표준은 부동소수점 연산의 정확한 규칙을 정의했고 그는 이 공로로 1989년 튜링상을 받았다.
표준은 단정밀도 32비트와 배정밀도 64비트의 비트 배치를 명시한다. 부호 비트, 지수 비트, 가수 비트가 정해진 길이로 할당된다. 배정밀도는 약 15에서 17자리의 십진 정확도를 제공한다. 일상적인 계산에서는 충분해 보이지만 누적되는 계산이나 극단적인 값의 계산에서는 이 한계가 드러난다.
금융 시스템이 부동소수점을 안 쓰는 이유
금융 시스템에서 부동소수점은 절대 사용되지 않는다. 1원의 오차도 누적되면 큰 금액이 된다. 1992년 패트리어트 미사일 시스템에서는 이 누적 오차가 인명 사고로 이어졌다. 시스템 가동 시간을 0.1초 단위로 카운트하면서 발생한 부동소수점 오차가 100시간 가동 후 약 0.34초의 시간 차이를 만들었고, 스커드 미사일의 예상 위치 계산이 어긋나면서 요격에 실패해 28명의 미군이 사망했다. 사건 직후 미국 회계감사원이 발간한 GAO 보고서 IMTEC-92-26은 이 사고의 기술적 원인을 부동소수점 누적 오차로 명시했다.
실수의 모든 연산은 근사이며 그 누적이 시스템을 위협한다
부동소수점 연산의 또 다른 함정은 결합법칙과 분배법칙이 성립하지 않는다는 점이다. (a 더하기 b) 더하기 c와 a 더하기 (b 더하기 c)가 다른 값이 될 수 있다. 매우 큰 수에 작은 수를 더하면 작은 수가 표현 가능한 정밀도 아래로 떨어져 사라진다. 이 현상을 흡수(Absorption)라고 부른다.
10억 번 더해도 안 변하는 합계
1억 더하기 0.0001을 1억 번 반복하면 결과는 1억 더하기 1만이 되어야 한다. 그러나 부동소수점에서는 매번 0.0001이 1억의 정밀도 아래로 떨어져 사라지고, 합계는 처음의 1억에서 거의 변하지 않을 수 있다. 이 문제를 피하기 위해 카한 합산 알고리즘(Kahan summation algorithm)이 개발되었다. 잘려나가는 부분을 별도 변수에 보관해 다음 연산에 더하는 방식이다.
과학 계산 라이브러리, 통계 패키지, 머신러닝 프레임워크 내부에는 이런 보정 알고리즘이 곳곳에 박혀 있다. 사용자는 합계 함수 하나를 호출하지만 그 안에서는 정확도 유지를 위한 여러 단계의 보정이 일어난다. NumPy의 sum 함수가 단순 누산이 아니라 짝수 인덱스와 홀수 인덱스를 분리해 합산한 뒤 마지막에 합치는 방식을 쓰는 이유도 같은 맥락이다.
GPS 위치 계산의 정밀도
GPS 시스템은 위성과 수신기 사이의 거리를 빛의 속도와 시간차로 계산한다. 빛의 속도가 초당 약 30만 킬로미터이므로 시간 측정의 1나노초 오차가 30센티미터의 위치 오차로 이어진다. 단정밀도 부동소수점으로는 이 정밀도를 유지할 수 없어 GPS 수신기는 배정밀도와 정수 연산을 조합해 사용한다. 일상에서 무심코 쓰는 위치 서비스의 정확도 뒤에는 수치 해석의 오랜 축적이 들어가 있다.
몬테카를로 시뮬레이션에서도 같은 문제가 등장한다
대규모 수치 시뮬레이션에서 부동소수점 오차의 누적은 결과의 신뢰도를 직접 좌우한다. 몬테카를로 시뮬레이션의 수렴성을 다룬 글에서 표본 크기와 신뢰 구간의 관계를 살펴봤는데, 표본을 아무리 늘려도 부동소수점 오차가 누적되면 통계적 수렴이 무의미해진다. 표본 백만 개의 평균을 단순히 더해서 백만으로 나누는 방식은 오차 누적에 매우 취약하다.
이 때문에 통계 소프트웨어는 평균을 계산할 때 단순 합산 후 나누기를 사용하지 않는다. 분산을 계산할 때도 정의대로 (각 값 빼기 평균)의 제곱을 평균 내는 방식 대신 웰포드(Welford) 알고리즘처럼 수치적으로 안정적인 변형을 사용한다. 같은 수학 공식이라도 부동소수점 환경에서는 표현 방식에 따라 정확도가 크게 달라진다는 사실이 수치 해석의 출발점이다. 정확한 수학과 정확하게 작동하는 코드 사이의 거리를 좁히는 일이 이 분야의 핵심 작업이다.