이번 Python 실험에서는 0.1 + 0.2가 0.3과 정확히 같지 않다.
출력은 0.30000000000000004다. 기초 산수의 오류처럼 보이지만 기계는 저장된 두 근삿값을 더하고 표현 가능한 값으로 다시 반올림했다. 화면의 십진 표기는 밑에 있는 이진 격자를 숨긴다. 컴퓨터가 셈을 못 한다고 결론내리기보다 격자를 이해하는 편이 유익하다.
화면의 숫자는 가까운 값의 짧은 이름일 수 있다.
이진수로 끝나지 않는 분수
십진법에서 1/3은 끝없이 반복된다. 이진법에도 그런 분수가 있다. 1/10은 유한한 2의 거듭제곱 합으로 쓰지 못한다. 일반적인 Python float는 유한한 유효숫자와 지수를 가진 binary64를 쓰므로 0.1과 0.2를 가까운 표현 가능 값으로 반올림한다.
0.3 리터럴은 별도로 반올림된다. 저장된 0.1과 0.2를 더하면 저장된 0.3 바로 위의 표현 가능 값으로 반올림된다. 이번 차이는 약 5.5511 × 10^-17이다. 아주 작지만 정확한 동등 비교는 이를 알아챈다.

표시 형식은 수리가 아니다
Python의 보통 표시는 다시 변환했을 때 같은 저장값이 되는 짧은 십진 표기를 고른다. 0.1이 보인다고 정확한 유리수 1/10이 저장됐다는 뜻은 아니다. 표시 자릿수를 줄이면 합이 0.3처럼 보이지만 내부 값은 변하지 않는다.
근사 측정이나 수치 계산에서는 정확한 동일성보다 명시한 허용 오차가 실제 질문에 맞을 수 있다. 오차에는 단위와 맥락이 필요하다. 큰 공학적 양에 적절한 문턱이 작은 신호에는 부적절할 수 있고, 0 부근에서는 상대 오차만 쓰는 데 주의해야 한다.
큰 수에는 더 넓은 틈이 있다
이웃한 float 간격은 크기에 따라 증가한다. 1 부근에서는 약 2.2204 × 10^-16이지만 10^16 부근에서는 2다. 여기에 1을 더하면 표현 가능한 이웃 사이에 놓인다. 이번 반올림에서는 10^16 + 1이 다시 10^16이 된다.
따라서 이 float 연산으로 (10^16 + 1) - 10^16을 계산하면 1 대신 0이다. 큰 수를 빼도 앞선 덧셈에서 잃은 작은 기여는 되찾지 못한다. 각 연산이 표현 규칙을 지키면서도 원하는 답 전체가 사라진다.
순서와 덧셈 방법도 중요하다
같은 스크립트에서 [10^16, 1, -10^16]에 math.fsum을 적용하면 1이 나온다. 단순히 이어 더하는 것보다 정보를 세심하게 보존한다. Python 정수로 표현한 같은 값도 정확히 1이 된다. 정수는 고정 53비트 유효숫자에 묶이지 않기 때문이다.
특별한 합 함수 하나가 모든 부동소수점 문제를 해결하는 것은 아니다. 이전 반올림, 가까운 근삿값의 뺄셈, 넘침, 알고리즘 구조도 중요하다. 이번 사례는 특정 손실과 여기서 피하는 방법 하나를 보인다. 수치 신뢰성은 마지막 표시가 아니라 전체 계산의 성질이다.

십진 입력은 다른 질문을 할 수 있다
문자열 “0.1”과 “0.2”에서 만든 Decimal은 현재 십진 문맥에서 정확히 “0.3”이 된다. 문자열이라는 점이 중요하다. 이미 반올림된 float로 Decimal을 만들면 이진 근사까지 포함한 그 값을 보존한다. 표현 선택은 입력 단계부터 시작된다.
Decimal에도 유한한 정밀도 문맥이 있어 모든 나눗셈을 정확히 나타내지는 못한다. 보존하려는 양과 규칙에 맞춰 골라야 한다. 정수는 개수에, 십진 표현은 명시적 십진 규칙에, 이진 float는 많은 근사 계산에 적합하다. 한 종류가 모든 목적을 만족하길 바라기보다 거래 조건을 아는 편이 낫다.
