Chapter 01

부동소수점과 오차

계산기에 0.1 + 0.2를 넣으면 0.3이 나오지만, 대부분의 프로그래밍 언어는 0.30000000000000004를 돌려줍니다. 버그가 아닙니다. 컴퓨터는 실수를 유한한 비트로 저장하고, 모든 연산 결과를 그 유한한 집합의 가장 가까운 원소로 반올림합니다. 수치해석은 바로 이 사실에서 출발합니다. 이 책 전체의 관점은 하나입니다. 컴퓨터는 유한한 수와 유한한 단계로 근사한다. 좋은 수치해석은 그 오차를 예측하고, 통제하고, 비용과 바꾸는 기술이다. 이 장은 그 첫 번째 오차, 곧 수를 저장하는 순간 생기는 반올림 오차를 해부합니다. 64개의 비트가 어떻게 수가 되는지, 표현할 수 있는 수가 수직선 위에 어떻게 흩어져 있는지, 단 하나의 상수 u≈1.1×10−16u \approx 1.1\times10^{-16}이 모든 연산의 오차를 어떻게 묶어 주는지, 그리고 그 작은 오차가 뺄셈과 긴 합산에서 어떻게 치명적으로 커지는지 차례로 봅니다.

이 수식이 없었다면어느 컴퓨터에서나 같은 답이 나오는 계산

1970년대까지 컴퓨터 회사마다 실수를 저장하는 형식과 반올림 규칙이 달랐습니다. 같은 프로그램이 기계에 따라 다른 답을 냈고, 어떤 기계에서는 x≠yx \ne y인데 x−y=0x - y = 0이 되기도 했습니다. 수치 소프트웨어를 만드는 사람은 기계마다 오차 분석을 다시 해야 했습니다. 1985년에 제정된 IEEE 754 표준은 수의 형식과 함께 "사칙연산과 제곱근의 결과는 정확한 값을 올바르게 반올림한 것이어야 한다"는 규칙을 정했습니다. 이 표준을 이끈 카한(William Kahan)은 1989년 튜링상을 받았습니다.

이 한 줄의 규칙 덕분에 모든 연산의 오차를 fl(x∘y)=(x∘y)(1+δ)\mathrm{fl}(x \circ y) = (x \circ y)(1 + \delta), ∣δ∣≤u|\delta| \le u라는 하나의 식으로 묶을 수 있게 되었습니다. 이 책의 모든 오차 분석, 곧 일기예보·구조 해석·회로 시뮬레이션·기계 학습 라이브러리가 믿고 쓰는 계산의 보증이 이 식 위에 서 있습니다.

무한히 많은 실수→64비트에 담기→fl(x) = x(1 + δ), |δ| ≤ u→오차를 예측하는 계산

0.1 + 0.2 ≠ 0.3: 수치해석의 출발점

0.1을 열 번 더하면 1이 아니다

센서 값을 0.1초마다 읽어 시간을 누적하는 프로그램이 있습니다. t = 0; for (i = 0; i < 10; i++) t += 0.1;를 실행하면 t는 0.9999999999999999이고, t == 1은 거짓입니다. 이 조건으로 반복을 멈추는 코드는 영원히 멈추지 않습니다. 왜 이런 일이 생길까요? 오차는 얼마나 클 수 있고, 어디서 커질까요?

원인은 간단합니다. 컴퓨터는 수를 2진법으로 저장하는데, 10진법의 0.1은 2진법에서 끝나지 않는 소수입니다. 1/3이 10진법에서 0.333…0.333\ldots로 끝나지 않는 것과 같습니다. 0.1에 2를 거듭 곱하며 정수 부분을 떼어 내면 2진 자리가 하나씩 나옵니다.

예제 1.1 (0.1의 2진 전개) 0.1에 2를 곱하고 정수 부분을 다음 자리로 떼어 내는 일을 되풀이하라.
  1. 0.1×2=0.2→00.1\times2 = 0.2 \to 0, 0.2×2=0.4→00.2\times2 = 0.4 \to 0, 0.4×2=0.8→00.4\times2 = 0.8 \to 0, 0.8×2=1.6→10.8\times2 = 1.6 \to 1 (남은 0.6), 0.6×2=1.2→10.6\times2 = 1.2 \to 1 (남은 0.2).
  2. 남은 값이 다시 0.2가 되었으므로 이후는 "0011"이 영원히 반복된다: 0.1=0.0 0011 0011 0011…20.1 = 0.0\,0011\,0011\,0011\ldots_2.
  3. 배정밀도는 첫 1부터 53자리만 저장하고 나머지를 반올림한다. 그래서 실제로 저장되는 수는
    fl(0.1)=0.1000000000000000055511151231257827021181583404541015625\mathrm{fl}(0.1) = 0.1000000000000000055511151231257827021181583404541015625
    이다. 오차는 약 5.55×10−185.55\times10^{-18}, 상대 오차로 5.55×10−175.55\times10^{-17}이다.
  4. 같은 방식으로 fl(0.2)=0.2000000000000000111…\mathrm{fl}(0.2) = 0.2000000000000000111\ldots, fl(0.3)=0.2999999999999999888…\mathrm{fl}(0.3) = 0.2999999999999999888\ldots이다. fl(0.1)+fl(0.2)\mathrm{fl}(0.1) + \mathrm{fl}(0.2)의 정확한 합 0.3000000000000000166…0.3000000000000000166\ldots은 이웃한 두 double의 정확히 한가운데라, 마지막 비트가 짝수인 쪽으로 반올림되어 0.3000000000000000444…0.3000000000000000444\ldots이 되고, 이는 fl(0.3)\mathrm{fl}(0.3)과 바로 옆의 다른 수다. 둘의 차이는 5.55×10−175.55\times10^{-17}, 곧 이 근처 수 간격(ulp) 한 칸이다.

이 현상은 프로그래밍 언어의 결함이 아니라 유한한 비트로 실수를 다루는 모든 계산의 성질입니다. 그리고 수치해석이 다루는 오차 가운데 하나일 뿐입니다. 실제 계산의 답이 참값과 다른 이유는 크게 네 가지입니다.

현실의 현상 다리, 대기, 회로 수학 모델 미분방정식, 적분 이산 알고리즘 유한 단계, 간격 h 컴퓨터 출력 유한 비트의 수 모델 오차 마찰 무시, 선형화 데이터 오차 측정값의 잡음 절단 오차 급수를 자르고 h로 근사 반올림 오차 이 장의 주제 수치해석이 다루는 것: 절단 + 반올림 + 그 전파
그림 1-1. 계산 결과가 참값과 다른 네 가지 이유. 모델 오차와 데이터 오차는 수치해석 이전의 문제이고, 수치해석은 절단 오차(무한 과정을 유한하게 자름)와 반올림 오차(실수를 유한 비트로 저장함), 그리고 앞선 오차가 계산을 거치며 커지거나 줄어드는 전파를 다룬다. 2장의 조건수가 전파를 재는 도구다.
정의 1.1 (절단 오차와 반올림 오차) 무한 과정(급수, 극한, 적분, 미분)을 유한한 단계로 대신해서 생기는 오차를 절단 오차(truncation error)라 한다. 예: ex≈∑k=0nxk/k!e^x \approx \sum_{k=0}^{n} x^k/k!, f′(x)≈(f(x+h)−f(x))/hf'(x) \approx (f(x + h) - f(x))/h. 실수를 유한한 자릿수의 수로 저장하거나 연산 결과를 유한 자릿수로 줄여서 생기는 오차를 반올림 오차(rounding error)라 한다.

절단 오차는 단계 수를 늘리거나 간격 hh를 줄이면 작아집니다. 3장부터 16장까지 이 책의 대부분은 절단 오차를 O(hp)O(h^p)로 재고 줄이는 이야기입니다. 반올림 오차는 반대로, 단계를 늘릴수록 쌓이고 hh를 줄일수록 커지기도 합니다(9장의 V자 그래프). 두 오차의 균형을 잡으려면 먼저 반올림 오차의 크기를 정확히 알아야 합니다. MathBook의 컴퓨터는 어떻게 틀리는가에서 맛본 내용을 여기서 엄밀하게 다시 세웁니다.

정수와 고정소수점의 한계

원자의 질량과 별의 질량을 같은 변수에

전자의 질량은 9.1×10−319.1\times10^{-31} kg, 태양의 질량은 2.0×10302.0\times10^{30} kg입니다. 한 시뮬레이션 안에서 두 값이 모두 나올 수 있습니다. 소수점 위치를 고정한 표현으로 둘을 모두 담으려면 몇 자리가 필요할까요?

가장 단순한 표현은 정수입니다. 32비트 부호 있는 정수(2의 보수)는 −231-2^{31}부터 231−1=2 147 483 6472^{31} - 1 = 2\,147\,483\,647까지를 간격 1로 빠짐없이 담습니다. 이 범위 안의 덧셈·곱셈은 정확합니다. 그러나 범위를 넘으면 경고 없이 값이 돌아갑니다. 2 147 483 647+12\,147\,483\,647 + 1은 32비트 연산에서 −2 147 483 648-2\,147\,483\,648이 됩니다(자바스크립트로 (2147483647 + 1) | 0).

소수를 담으려면 고정소수점(fixed point) 표현을 씁니다. 32비트 중 아래 16비트를 소수 부분으로 정하면(Q16.16 형식) 저장된 정수 NN이 값 N/216N/2^{16}을 뜻합니다. 간격은 2−16≈1.5×10−52^{-16} \approx 1.5\times10^{-5}로 일정하고, 범위는 ±32768\pm 32768입니다. 신호 처리 칩이나 부동소수점 장치가 없는 마이크로컨트롤러는 지금도 이 방식을 씁니다. 하지만 간격이 절대적으로 고정되어 있다는 것이 치명적입니다.

과학자들이 오래전부터 쓰는 해법은 과학적 표기법입니다. 9.1×10−319.1\times10^{-31}처럼 유효 숫자 몇 자리와 지수를 따로 적으면, 크기와 상관없이 상대 정밀도가 일정합니다. 컴퓨터의 부동소수점은 이것을 2진법으로 옮긴 것입니다. 소수점이 지수에 따라 "떠다닌다"(floating)고 해서 붙은 이름입니다.

고정소수점: 간격이 일정 016 작은 수 근처에서 상대 오차가 크다 부동소수점: 간격이 크기에 비례 024816 2의 거듭제곱마다 간격이 두 배
그림 1-2. 같은 개수의 수를 고정소수점은 고르게, 부동소수점은 2의 거듭제곱 구간마다 같은 개수씩(여기서는 4개) 놓는다. 그래서 부동소수점의 상대 간격은 크기와 거의 무관하다.

IEEE 754 배정밀도: 부호·지수·가수

정의 1.2 (부동소수점 수 체계) 밑 β\beta, 정밀도 pp, 지수 범위 [emin⁡,emax⁡][e_{\min}, e_{\max}]의 부동소수점 수(floating-point number)는
x=± (d0.d1d2…dp−1)β×βe,0≤di≤β−1,emin⁡≤e≤emax⁡x = \pm\, (d_0.d_1d_2\ldots d_{p-1})_\beta \times \beta^{e}, \qquad 0 \le d_i \le \beta - 1,\quad e_{\min} \le e \le e_{\max}
꼴의 수다. d0.d1…dp−1d_0.d_1\ldots d_{p-1}을 가수(significand, mantissa), ee를 지수(exponent)라 한다. d0≠0d_0 \ne 0이면 정규화되었다고 한다. 이런 수 전체와 0의 집합을 F(β,p,emin⁡,emax⁡)\mathbb F(\beta, p, e_{\min}, e_{\max})로 쓴다.

β=2\beta = 2이면 정규화된 수의 첫 자리 d0d_0은 언제나 1입니다. 그래서 저장할 필요가 없습니다. 이 "숨은 비트" 덕분에 52비트로 53자리의 정밀도를 얻습니다. IEEE 754의 배정밀도(binary64, double)가 바로 F(2,53,−1022,1023)\mathbb F(2, 53, -1022, 1023)입니다.

정의 1.3 (IEEE 754 binary64) 64비트를 부호 1비트 ss, 지수 11비트 EE(부호 없는 정수 0–2047), 소수 52비트 f=f1f2…f52f = f_1f_2\ldots f_{52}로 나누고 다음과 같이 해석한다.
x={(−1)s×(1.f1f2…f52)2×2E−1023,1≤E≤2046 (정규 수)(−1)s×(0.f1f2…f52)2×2−1022,E=0 (0과 비정규 수)(−1)s×∞ (f=0),NaN (f≠0),E=2047x = \begin{cases} (-1)^s \times (1.f_1f_2\ldots f_{52})_2 \times 2^{E - 1023}, & 1 \le E \le 2046 \ (\text{정규 수}) \\ (-1)^s \times (0.f_1f_2\ldots f_{52})_2 \times 2^{-1022}, & E = 0 \ (\text{0과 비정규 수}) \\ (-1)^s \times \infty \ (f = 0), \quad \text{NaN}\ (f \ne 0), & E = 2047 \end{cases}
1023을 지수 편향(exponent bias)이라 한다. 편향을 쓰면 지수 비트를 부호 없는 정수로 비교할 수 있어, 양수끼리의 크기 비교가 64비트 정수 비교와 같아진다.
예제 1.2 (−2.5와 0.1을 비트로)
  1. −2.5=−10.12=−1.012×21-2.5 = -10.1_2 = -1.01_2\times2^1. 부호 s=1s = 1, 지수 E=1+1023=1024=100000000002E = 1 + 1023 = 1024 = 10000000000_2, 소수 f=0100…0f = 0100\ldots0. 16진수로 c004000000000000.
  2. 0.1=1.1001 1001 1001…2×2−40.1 = 1.1001\,1001\,1001\ldots_2 \times 2^{-4}. E=−4+1023=1019=011111110112E = -4 + 1023 = 1019 = 01111111011_2. 소수 52비트는 10011001 반복이고, 53번째 이후 비트가 1001…1001\ldots(절반보다 큼)이라 마지막 자리를 올린다: …1001 1001 1010\ldots1001\,1001\,1010. 16진수로 3fb999999999999a. 끝의 a(1010)가 올림의 흔적이다.
  3. 1=1.02×201 = 1.0_2\times 2^0은 E=1023E = 1023, f=0f = 0, 3ff0000000000000.
형식비트지수 비트정밀도 pε = 21−p최댓값최소 정규 수
반정밀도 binary16165119.77×10⁻⁴655046.10×10⁻⁵
bfloat16 (IEEE 아님)16887.81×10⁻³3.39×10³⁸1.18×10⁻³⁸
단정밀도 binary32328241.19×10⁻⁷3.40×10³⁸1.18×10⁻³⁸
배정밀도 binary646411532.22×10⁻¹⁶1.80×10³⁰⁸2.23×10⁻³⁰⁸
4배정밀도 binary128128151131.93×10⁻³⁴1.19×10⁴⁹³²3.36×10⁻⁴⁹³²

배정밀도의 53비트는 10진법으로 53log⁡102≈15.9553\log_{10}2 \approx 15.95자리입니다. "double은 십진 15–16자리"라는 말이 여기서 나옵니다. 단정밀도는 약 7자리입니다. 기계 학습 가속기가 즐겨 쓰는 16비트 형식은 2자리에서 3자리 남짓이라, 2장의 혼합 정밀도 기법과 함께 써야 합니다.

아래 편집기에서 비트를 직접 눌러 보세요. 64칸 각각이 값에 어떻게 기여하는지, 지수 비트를 바꾸면 값이 2배씩 뛰고 소수 마지막 비트를 바꾸면 값이 ulp 한 칸만 움직이는 것을 볼 수 있습니다.

SIMULATOR

64비트 편집기: 비트를 눌러 배정밀도 수 만들기

부호 s (1비트)지수 E (11비트)소수 f (52비트)
16진수—
종류—
값 (최단 표기)—
ulp (다음 수까지)—
해볼 것: ① 0.1에서 '다음 수 ▶'를 누르면 0.10000000000000002가 됩니다. 그 사이에는 double이 없습니다. ② 1에서 지수의 마지막 비트를 누르면 0.5(지수 −1)가 되고, 소수의 마지막(오른쪽 끝) 비트를 누르면 1 + 2⁻⁵²가 됩니다. ③ 지수를 모두 0으로 만들면 비정규 수, 모두 1로 만들면 ∞나 NaN이 됩니다. 모델: 값은 브라우저의 Float64(IEEE 754 binary64) 그대로이고, 정확한 10진값은 정수 산술로 계산합니다.

수직선 위의 부동소수점 수: 간격은 크기에 비례한다

부동소수점 수는 수직선 위에 고르게 놓여 있지 않습니다. 지수가 같은 수들, 곧 구간 [2e,2e+1)[2^e, 2^{e+1}) 안의 수들은 가수의 마지막 자리 한 칸씩 떨어져 있습니다. 지수가 하나 커지면 같은 개수의 수가 두 배 넓은 구간에 퍼집니다.

정리 1.4 (부동소수점 수의 간격) F(2,p,emin⁡,emax⁡)\mathbb F(2, p, e_{\min}, e_{\max})에서 emin⁡≤e≤emax⁡e_{\min} \le e \le e_{\max}일 때 구간 [2e,2e+1)[2^e, 2^{e+1}) 안에는 정확히 2p−12^{p-1}개의 수 2e, 2e+2e−p+1, 2e+2⋅2e−p+1, …2^e,\ 2^e + 2^{e-p+1},\ 2^e + 2\cdot2^{e-p+1},\ \dots가 간격 2e−p+12^{e-p+1}로 놓인다. 따라서 이 구간의 수 xx와 다음 수의 간격을 ∣x∣|x|로 나눈 상대 간격은
2−p<2e−p+1∣x∣≤21−p2^{-p} \lt \frac{2^{e-p+1}}{|x|} \le 2^{1-p}
로, 크기와 무관하게 두 배 범위 안에 있다.

증명. 정규화된 가수 1.f1…fp−11.f_1\ldots f_{p-1}은 p−1p-1개의 비트로 2p−12^{p-1}가지 값을 가지며, 이웃한 가수의 차는 2−(p−1)2^{-(p-1)}이다. 여기에 2e2^e를 곱하면 간격 2e−p+12^{e-p+1}이다. 2e≤∣x∣<2e+12^e \le |x| \lt 2^{e+1}로 나누면 상대 간격의 범위가 나온다. ∎

배정밀도에서 1 다음 수는 1+2−521 + 2^{-52}, 1000 근처의 간격은 1.14×10−131.14\times10^{-13}, 101610^{16} 근처의 간격은 2입니다. 그래서 1016+110^{16} + 1은 101610^{16}으로 반올림되어 사라집니다. 정수를 빠짐없이 담는 한계는 253=9 007 199 254 740 9922^{53} = 9\,007\,199\,254\,740\,992이고, 253+12^{53} + 1은 double로 표현할 수 없습니다. 자바스크립트의 Number.MAX_SAFE_INTEGER가 253−12^{53} - 1인 이유입니다.

아래 시뮬레이터는 정밀도 pp와 지수 범위를 작게 줄인 "장난감 부동소수점 체계"를 수직선에 그립니다. 64비트 체계도 정확히 같은 모양이고, 단지 눈금이 2522^{52}배 촘촘할 뿐입니다.

SIMULATOR

장난감 부동소수점 수직선: 간격과 반올림

수직선을 클릭·드래그해 x를 고르기
가로축
정규 수비정규 수x와 fl(x)
양수 개수—
최댓값 · 최소 정규 수—
x → fl(x)—
상대 오차 / u—
해볼 것: ① 선형 축에서 2의 거듭제곱(긴 눈금)을 지날 때마다 간격이 두 배가 되는 것을 보세요. ② 로그 축으로 바꾸면 구간마다 눈금 수가 같아 거의 고르게 보입니다. 상대 간격이 일정하다는 뜻입니다. ③ x를 끌며 상대 오차가 언제나 u = 2−p 이하인지 확인하세요. 단, 비정규 수 구간과 최댓값 너머에서는 이 보장이 깨집니다. 모델: 𝔽(2, p, 1 − emax, emax), 가장 가까운 수로 반올림(동률이면 짝수), 최댓값을 넘는 x는 ∞.

기계 엡실론, 반올림 단위, ulp

1에 더해도 1이 그대로인 가장 큰 수

"충분히 작은 수"를 판단하는 코드, 예를 들어 반복을 멈추는 조건 |x_new − x| < tol에서 tol을 10−2010^{-20}으로 두면 어떻게 될까요? x≈1x \approx 1인 근처에서 두 double의 차이는 0이 아니면 최소 2.2×10−162.2\times10^{-16}이므로 이 조건은 xnew=xx_{\text{new}} = x일 때만 참이 됩니다. 컴퓨터가 구별할 수 있는 상대 차이의 한계를 알아야 합니다.

정의 1.5 (기계 엡실론, 반올림 단위, ulp)
  • 기계 엡실론(machine epsilon) ε\varepsilon은 1과 1 다음의 부동소수점 수 사이의 간격이다. F(2,p,⋅,⋅)\mathbb F(2, p, \cdot, \cdot)에서 ε=21−p\varepsilon = 2^{1-p}, 배정밀도에서 ε=2−52≈2.22×10−16\varepsilon = 2^{-52} \approx 2.22\times10^{-16}.
  • 반올림 단위(unit roundoff) u=ε/2=2−pu = \varepsilon/2 = 2^{-p}. 배정밀도에서 u=2−53≈1.11×10−16u = 2^{-53} \approx 1.11\times10^{-16}.
  • 부동소수점 수 xx의 ulp(unit in the last place)는 xx가 속한 구간 [2e,2e+1)[2^e, 2^{e+1})의 간격 2e−p+1=ε⋅2e2^{e-p+1} = \varepsilon\cdot2^e이다.

IEEE 754의 기본 반올림은 가장 가까운 수로, 동률이면 마지막 비트가 0인(짝수) 쪽으로(round to nearest, ties to even)입니다. 동률을 늘 위로 올리면 긴 계산에서 오차가 한쪽으로 쏠리기 때문에, 짝수 쪽을 골라 평균적으로 상쇄되게 한 것입니다. 이 반올림에서 오차 상한은 간격의 절반입니다.

정리 1.6 (반올림 오차의 상한) xx가 정규 수 범위 안의 실수이고 fl(x)\mathrm{fl}(x)가 가장 가까운 부동소수점 수로의 반올림이면
fl(x)=x(1+δ),∣δ∣≤u=2−p.\mathrm{fl}(x) = x(1 + \delta), \qquad |\delta| \le u = 2^{-p}.

증명. 2e≤∣x∣<2e+12^e \le |x| \lt 2^{e+1}이라 하자. 정리 1.4에 따라 이 구간의 간격은 2e−p+12^{e-p+1}이므로 가장 가까운 수까지의 거리는 그 절반 2e−p2^{e-p} 이하다. 따라서 ∣fl(x)−x∣/∣x∣≤2e−p/2e=2−p|\mathrm{fl}(x) - x|/|x| \le 2^{e-p}/2^e = 2^{-p}. δ=(fl(x)−x)/x\delta = (\mathrm{fl}(x) - x)/x로 두면 된다. ∎

이 정리는 수 하나를 저장할 때의 이야기입니다. IEEE 754는 여기에 결정적인 약속을 더합니다. +,−,×,÷, +, -, \times, \div, \sqrt{\ }의 결과는 정확한 결과를 반올림한 값이어야 합니다(correctly rounded). 그러므로 부동소수점 수 x,yx, y에 대해

fl(x∘y)=(x∘y)(1+δ),∣δ∣≤u,∘∈{+,−,×,÷}.\mathrm{fl}(x \circ y) = (x \circ y)(1 + \delta), \qquad |\delta| \le u, \qquad \circ \in \{+, -, \times, \div\}.
이것이 표준 반올림 오차 모형이다. 2장에서 이 식 하나로 합산·내적·다항식 계산의 오차를 분석한다. 주의: δ\delta는 연산마다 다른, 크기만 아는 수다.

기계 엡실론은 실험으로 찾을 수 있습니다. 1에 ee를 더해 1보다 커지는지 확인하며 ee를 반씩 줄이면 됩니다.

// 기계 엡실론 찾기: 1 + e/2 가 1과 구별되지 않을 때까지 e를 반씩 줄인다
e ← 1
while fl(1 + e/2) > 1 do
    e ← e / 2
end
return e          // double: 52번 반복 후 e = 2^(-52)
예제 1.3 (엡실론 찾기의 마지막 세 단계) 배정밀도에서 위 반복을 따라가라.
  1. 50번 반복 후 e=2−50e = 2^{-50}: fl(1+2−51)=1.0000000000000004>1\mathrm{fl}(1 + 2^{-51}) = 1.0000000000000004 \gt 1이므로 계속.
  2. 51번 후 e=2−51e = 2^{-51}: fl(1+2−52)=1.0000000000000002>1\mathrm{fl}(1 + 2^{-52}) = 1.0000000000000002 \gt 1이므로 계속.
  3. 52번 후 e=2−52e = 2^{-52}: 1+2−531 + 2^{-53}은 1과 1+2−521 + 2^{-52}의 정확히 한가운데다. 동률이므로 마지막 비트가 0인 1로 반올림되어 fl(1+2−53)=1\mathrm{fl}(1 + 2^{-53}) = 1. 멈춘다. 결과 e=2−52=2.220446049250313×10−16e = 2^{-52} = 2.220446049250313\times10^{-16}, 자바스크립트의 Number.EPSILON과 같다.
SIMULATOR

기계 엡실론 찾기: 1이 움직이지 않을 때까지 반으로

형식
kefl(x + e/2)> x ?
반복 횟수 k—
현재 e—
e / x—
해볼 것: ① '자동'으로 끝까지 돌려 double은 52번, single은 23번 만에 멈추는 것을 보세요. 위 캔버스에서 1비트가 가수의 오른쪽 끝으로 밀려가다 떨어집니다. ② x = 1000이면 멈춘 e가 ulp(1000) = 2−43이고, e/x ≈ 0.51ε로 ε/2와 ε 사이입니다(정리 1.4). ③ x = 0.1도 해 보세요. 모델: 시작 e는 x보다 크거나 같은 가장 작은 2의 거듭제곱, single은 Math.fround로 매 연산을 반올림합니다.

오버플로·언더플로·비정규 수, Inf와 NaN

직각삼각형의 빗변이 무한대?

두 변이 3×102003\times10^{200}과 4×102004\times10^{200}인 직각삼각형의 빗변은 5×102005\times10^{200}으로 double 범위 안에 넉넉히 들어갑니다. 그런데 Math.sqrt(x*x + y*y)는 Infinity를 돌려줍니다. 중간값 x2=9×10400x^2 = 9\times10^{400}이 범위를 넘었기 때문입니다. 답이 아니라 중간 계산이 범위를 벗어나는 일을 어떻게 막을까요?

지수의 범위가 유한하므로 표현할 수 있는 크기에도 위아래 끝이 있습니다.

정의 1.7 (오버플로, 언더플로, 비정규 수) 결과의 크기가 최댓값 xmax⁡=(2−2−52)×21023≈1.80×10308x_{\max} = (2 - 2^{-52})\times2^{1023} \approx 1.80\times10^{308}을 넘으면 오버플로(overflow)이고 결과는 ±∞\pm\infty가 된다. 크기가 최소 정규 수 xmin⁡=2−1022≈2.23×10−308x_{\min} = 2^{-1022} \approx 2.23\times10^{-308}보다 작으면 언더플로(underflow)다. 이때 IEEE 754는 0으로 바로 떨어뜨리지 않고, 지수를 −1022-1022에 고정한 채 첫 자리를 0으로 허용하는 비정규 수(subnormal number) (0.f)2×2−1022(0.f)_2\times 2^{-1022}를 쓴다. 가장 작은 양의 비정규 수는 2−1074≈4.94×10−3242^{-1074} \approx 4.94\times10^{-324}이다.

비정규 수는 0과 xmin⁡x_{\min} 사이를 간격 2−10742^{-1074}로 고르게 채웁니다(그림의 주황 눈금). 이것을 점진적 언더플로(gradual underflow)라 합니다. 덕분에 "x≠yx \ne y이면 x−y≠0x - y \ne 0"이 언제나 성립합니다. 대가는 정밀도입니다. 비정규 수에서는 앞자리 0들이 가수를 차지하므로 상대 오차 상한 uu가 성립하지 않고, 2−10742^{-1074} 근처의 수는 유효 숫자가 한 자리뿐입니다.

연산이 수학적으로 정의되지 않으면 결과는 NaN(Not a Number)입니다. 특수 값의 규칙은 대부분 극한의 상식을 따릅니다.

연산결과설명
1e308 × 10Infinity오버플로
1 / 0, −1 / 0Infinity, −Infinity0으로 나누기는 오류가 아니라 부호 있는 무한대
1 / −0−Infinity−0은 0과 같다고 비교되지만(−0 == 0) 부호를 기억한다
0 / 0, ∞ − ∞, 0 × ∞, √−1NaN정의되지 않는 연산
NaN == NaNfalseNaN은 자기 자신과도 같지 않다(x != x로 검사)
2⁻¹⁰²² / 21.11×10⁻³⁰⁸비정규 수(점진적 언더플로)
2⁻¹⁰⁷⁴ / 20완전한 언더플로(동률 → 짝수인 0)

NaN과 Inf는 "계산을 멈추지 않고 문제를 끝까지 운반"하도록 설계되었습니다. 계산 도중 한 번 NaN이 생기면 이후 거의 모든 연산 결과가 NaN이 되므로, 마지막에 한 번만 검사하면 됩니다. 반대로 말하면 NaN이 나왔을 때는 처음 생긴 곳을 찾아야 합니다.

빗변 문제의 처방은 척도 조정(scaling)입니다. 큰 쪽 m=max⁡(∣x∣,∣y∣)m = \max(|x|, |y|)으로 나누어 계산하면 중간값이 1 근처에 머뭅니다.

// 오버플로·언더플로 없는 빗변 sqrt(x^2 + y^2)
m ← max(|x|, |y|)
if m = 0 then return 0
r ← min(|x|, |y|) / m          // 0 ≤ r ≤ 1
return m · sqrt(1 + r·r)
SIMULATOR

범위의 양 끝: 빗변 계산의 오버플로와 언더플로

비정규 수 구간정규 수 구간오버플로(∞)·언더플로(0)
x² (중간값)—
√(x² + y²) 그대로—
척도 조정 m√(1 + r²)—
참값 5·10k—
해볼 것: ① k를 154보다 크게 하면 x²이 10³⁰⁸을 넘어 그대로 계산한 빗변이 ∞가 됩니다. ② k를 −154보다 작게 하면 x²이 비정규 수가 되면서 정확도를 잃고, −162보다 작으면 0이 되어 빗변이 0이 됩니다. ③ 척도 조정은 x 자체가 비정규 수가 되는 k ≈ −308 아래까지 정확합니다. 모델: 막대는 각 값의 log₁₀ 크기, 모두 배정밀도 연산.

절대 오차, 상대 오차, 유효 숫자

오차의 크기를 말하는 방법은 두 가지입니다. 0.001의 오차는 지구–달 거리에서는 놀라운 정확도이고, 반도체 게이트 길이(수십 나노미터)를 미터로 잴 때는 쓸모없는 값입니다. 크기에 견준 오차가 필요합니다.

정의 1.8 (절대 오차, 상대 오차, 유효 숫자) 참값 x≠0x \ne 0의 근삿값 x^\hat x에 대해 절대 오차(absolute error)는 ∣x^−x∣|\hat x - x|, 상대 오차(relative error)는 ∣x^−x∣/∣x∣|\hat x - x|/|x|이다. 상대 오차가 5×10−q5\times10^{-q} 이하인 가장 큰 정수 qq를 x^\hat x의 (10진) 유효 숫자(significant digits) 개수라 한다. 대략 q≈−log⁡10(상대 오차)q \approx -\log_{10}(\text{상대 오차})이다.
예제 1.4 (원주율의 두 근삿값) π=3.14159265…\pi = 3.14159265\ldots
  1. 22/7=3.142857…22/7 = 3.142857\ldots: 절대 오차 1.264×10−31.264\times10^{-3}, 상대 오차 4.02×10−4≤5×10−44.02\times10^{-4} \le 5\times10^{-4}. 유효 숫자 3자리(3.14).
  2. 355/113=3.1415929…355/113 = 3.1415929\ldots: 절대 오차 2.67×10−72.67\times10^{-7}, 상대 오차 8.49×10−8≤5×10−78.49\times10^{-8} \le 5\times10^{-7}. 유효 숫자 7자리.
  3. 배정밀도의 Math.PI는 상대 오차 uu 이하, 유효 숫자 약 16자리다. 정리 1.6은 "부동소수점에 저장하면 유효 숫자 약 16자리가 보장된다"는 뜻이다.

참값이 0에 가까우면 상대 오차는 쓸 수 없으므로 절대 오차를 씁니다. 실무의 멈춤 조건이 흔히 ∣x^−x∣≤atol+rtol⋅∣x∣|\hat x - x| \le \mathrm{atol} + \mathrm{rtol}\cdot|x|처럼 둘을 섞는 이유입니다(3장, 11장). 상대 오차의 중요한 성질은 곱셈과 나눗셈에서 거의 더해진다는 것입니다. x^=x(1+δ1)\hat x = x(1 + \delta_1), y^=y(1+δ2)\hat y = y(1 + \delta_2)이면 x^y^=xy(1+δ1+δ2+δ1δ2)\hat x\hat y = xy(1 + \delta_1 + \delta_2 + \delta_1\delta_2). 곱셈·나눗셈은 상대 오차를 키우지 않습니다. 문제는 덧셈과 뺄셈입니다.

자릿수 상쇄: 비슷한 두 수의 뺄셈

근의 공식이 틀린 답을 낸다

x2−108x+1=0x^2 - 10^8x + 1 = 0의 두 근은 약 10810^8과 10−810^{-8}입니다. 근의 공식 x=(108±1016−4)/2x = \bigl(10^8 \pm \sqrt{10^{16} - 4}\bigr)/2를 그대로 double로 계산하면 작은 근이 7.45×10−97.45\times10^{-9}으로 나옵니다. 상대 오차 25%, 유효 숫자가 하나도 맞지 않습니다. 연산 하나하나의 오차는 10−1610^{-16}인데 어떻게 이런 일이 생길까요?

뺄셈 자체는 정확합니다. 아주 가까운 두 부동소수점 수의 차는 반올림 없이 정확히 계산됩니다(스테르베즈 보조정리(Sterbenz lemma): y/2≤x≤2yy/2 \le x \le 2y이면 x−yx - y는 부동소수점 수다). 문제는 뺄셈에 들어가는 값이 이미 오차를 품고 있다는 것입니다. 앞자리가 같은 두 수를 빼면 같은 앞자리가 사라지고, 오차가 섞인 뒷자리만 남아 앞으로 올라옵니다. 이것을 자릿수 상쇄(catastrophic cancellation)라 합니다.

정리 1.9 (뺄셈의 오차 증폭) x^=x(1+δx)\hat x = x(1 + \delta_x), y^=y(1+δy)\hat y = y(1 + \delta_y), ∣δx∣,∣δy∣≤δ|\delta_x|, |\delta_y| \le \delta이고 x≠yx \ne y이면, 뺄셈을 정확히 하더라도
∣(x^−y^)−(x−y)∣∣x−y∣=∣xδx−yδy∣∣x−y∣≤∣x∣+∣y∣∣x−y∣ δ.\frac{|(\hat x - \hat y) - (x - y)|}{|x - y|} = \frac{|x\delta_x - y\delta_y|}{|x - y|} \le \frac{|x| + |y|}{|x - y|}\,\delta.

증폭 인자 (∣x∣+∣y∣)/∣x−y∣(|x| + |y|)/|x - y|는 x≈yx \approx y일 때 한없이 커진다. 예를 들어 xx와 yy가 앞 10자리가 같으면 인자는 약 2×10102\times10^{10}이고, 16자리 중 10자리를 잃어 6자리만 남는다. (이 인자는 2장에서 뺄셈의 조건수로 다시 나온다.)

근의 공식에서는 −b=108-b = 10^8과 b2−4ac=99999999.99999998\sqrt{b^2 - 4ac} = 99999999.99999998이 앞 16자리가 거의 같습니다.  \sqrt{\ }의 결과에 있던 10−1610^{-16} 수준의 상대 오차가 108/10−810^8/10^{-8}배 증폭되어 결과를 망칩니다. 처방은 빼지 않는 식으로 바꾸는 것입니다. 두 근의 곱이 c/ac/a라는 비에트 공식을 쓰면, 상쇄가 없는 큰 근을 먼저 구하고 작은 근을 나눗셈으로 얻을 수 있습니다.

// 상쇄 없는 이차방정식 ax^2 + bx + c = 0 (실근, a ≠ 0)
d ← sqrt(b·b − 4·a·c)
q ← −(b + sign(b)·d) / 2     // b와 sign(b)·d는 부호가 같다 → 더해도 상쇄 없음
x1 ← q / a                   // 크기가 큰 근
x2 ← c / q                   // 작은 근: 비에트 공식 x1·x2 = c/a
예제 1.5 (이차방정식 x2−bx+1=0x^2 - bx + 1 = 0의 작은 근) 근의 공식 (b−b2−4)/2(b - \sqrt{b^2 - 4})/2와 위 알고리즘 2/(b+b2−4)2/(b + \sqrt{b^2 - 4})를 double로 비교한다.
b근의 공식 그대로상대 오차상쇄 없는 식상대 오차
10²0.0100010002000487931.2×10⁻¹³0.010001000200050014< 10⁻¹⁶
10⁴0.000100000001111766321.1×10⁻⁹0.00010000000100000001< 10⁻¹⁶
10⁶1.00000761449337×10⁻⁶7.6×10⁻⁶1.000000000001×10⁻⁶< 10⁻¹⁶
10⁸7.450580596923828×10⁻⁹2.5×10⁻¹1×10⁻⁸< 10⁻¹⁶

표의 둘째 열은 bb가 100배 커질 때마다 오차가 약 10410^4배 커집니다. 증폭 인자 ≈b2\approx b^2과 맞습니다. 같은 일이 1−cos⁡x1 - \cos x에서도 일어납니다. x=10−8x = 10^{-8}이면 cos⁡x=1−5×10−17\cos x = 1 - 5\times10^{-17}인데 이 값은 1과 uu보다 가까워 fl(cos⁡x)=1\mathrm{fl}(\cos x) = 1이고, 결과는 0입니다. 참값은 5×10−175\times10^{-17}입니다. 삼각 항등식 1−cos⁡x=2sin⁡2(x/2)1 - \cos x = 2\sin^2(x/2)는 뺄셈이 없어 모든 xx에서 정확합니다.

x10⁻²10⁻⁴10⁻⁶10⁻⁷10⁻⁸
1 − cos x의 상대 오차2.9×10⁻¹³5.2×10⁻⁹8.9×10⁻⁵8.0×10⁻⁴1 (결과 0)
2 sin²(x/2)의 상대 오차< 2u< 2u< 2u< 2u< 2u
SIMULATOR

상쇄 오차 그래프: x → 0에서 잃는 자릿수

그대로 계산다시 쓴 식예측 (|x|+|y|)/|x−y| · u
그대로—
다시 쓴 식—
상대 오차—
남은 유효 숫자—
해볼 것: ① '1 − cos x'에서 x를 줄이면 오차가 x⁻² 기울기(로그–로그에서 −2)로 커지다가 x ≈ 10⁻⁸에서 결과가 0이 되어 상대 오차 1에 붙습니다. ② 빨간 점들이 회색 예측선 근처에서 들쭉날쭉한 것은 반올림 오차의 부호가 x마다 다르기 때문입니다. 크기의 경향은 정리 1.9가 정확히 맞힙니다. ③ 이차방정식에서는 b = 1/x입니다(x = 10⁻⁸이면 b = 10⁸). 표의 값을 슬라이더로 다시 찾아 보세요. 모델: 참값은 다시 쓴 식(상쇄 없음)으로 계산합니다.

상쇄가 어디서 일어나는지 한 단계씩 보면 더 분명합니다. 아래 표는 근의 공식의 중간값을 17자리로 보여 주고, 뺄셈에서 사라지는 같은 앞자리를 밑줄로 표시합니다.

SIMULATOR

근의 공식 해부: 사라지는 자릿수를 세기

작은 근 계산
방정식—
작은 근 (계산)—
상대 오차—
상쇄된 자릿수—
해볼 것: ① m을 1씩 올리면 −b와 √D가 일치하는 앞자리가 약 2자리씩 늘고(두 값의 지수가 같으면 밑줄로 표시), 그만큼 결과의 유효 숫자가 줄어듭니다(16 − 상쇄 자릿수 ≈ 남은 자릿수). ② '비에트 공식'으로 바꾸면 −b + √D 대신 −b − √D(같은 부호끼리 더하기)를 쓰므로 상쇄가 없습니다. ③ m = 8 근처에서 근의 공식은 √D의 마지막 비트 하나에 좌우됩니다. 모델: x² + bx + c = 0, a = 1, 참값은 비에트 공식(오차 u 수준).

오차의 누적과 합산 순서, 카한 합산

발산하는 급수가 수렴한다?

조화급수 1+12+13+⋯1 + \frac12 + \frac13 + \cdots는 발산합니다. 그런데 단정밀도로 차례대로 더하면 합이 15.40368315.403683에서 멈춥니다. k=2 097 152k = 2\,097\,152번째 항 1/k≈4.8×10−71/k \approx 4.8\times10^{-7}이 합 15.4의 ulp의 절반보다 작아져서, 더해도 합이 바뀌지 않기 때문입니다. 긴 합산에서는 덧셈마다 생기는 작은 오차와 "작은 수가 큰 수에 먹히는" 현상이 쌓입니다.

nn개의 수를 차례대로 더하는 재귀 합산 s1=x1s_1 = x_1, sk=fl(sk−1+xk)s_k = \mathrm{fl}(s_{k-1} + x_k)의 오차를 표준 모형으로 추적해 봅시다. 각 덧셈에서 sk=(sk−1+xk)(1+δk)s_k = (s_{k-1} + x_k)(1 + \delta_k), ∣δk∣≤u|\delta_k| \le u입니다.

정리 1.10 (재귀 합산의 오차 한계) 위 재귀 합산의 결과 S^=sn\hat S = s_n과 참값 S=∑xiS = \sum x_i에 대해
∣S^−S∣≤(n−1) u∑i=1n∣xi∣+O(u2).|\hat S - S| \le (n - 1)\,u \sum_{i=1}^{n} |x_i| + O(u^2).

증명의 핵심. 식을 펼치면 S^=x1∏k=2n(1+δk)+x2∏k=2n(1+δk)+x3∏k=3n(1+δk)+⋯+xn(1+δn)\hat S = x_1\prod_{k=2}^{n}(1 + \delta_k) + x_2\prod_{k=2}^{n}(1 + \delta_k) + x_3\prod_{k=3}^{n}(1 + \delta_k) + \cdots + x_n(1 + \delta_n). 항 xix_i는 n−i+1n - i + 1번(x1x_1은 n−1n - 1번) 반올림을 겪는다. ∏(1+δk)=1+θ\prod(1 + \delta_k) = 1 + \theta, ∣θ∣≤mu+O(u2)|\theta| \le mu + O(u^2)을 쓰면 ∣S^−S∣≤∑i(n−i+1)u∣xi∣≤(n−1)u∑∣xi∣|\hat S - S| \le \sum_i (n - i + 1)u|x_i| \le (n - 1)u\sum|x_i| (첫 항 보정 포함). ∎

이 정리에서 두 가지 교훈이 나옵니다. 첫째, 먼저 더한 항일수록 더 많은 반올림을 겪으므로, 크기가 작은 항부터 더하면 큰 mm이 작은 ∣xi∣|x_i|에 곱해져 실제 오차가 줄어듭니다. 둘째, 상한이 ∑∣xi∣\sum|x_i|에 비례하므로, 부호가 섞여 ∣S∣≪∑∣xi∣|S| \ll \sum|x_i|인 합(교대 급수)은 상대 오차가 클 수 있습니다. 뺄셈 상쇄의 합산판입니다.

순서를 바꾸는 것보다 강력한 방법은 잃어버린 부분을 따로 모으는 것입니다. 카한(Kahan)이 1965년에 발표한 보정 합산은 덧셈마다 반올림으로 떨어져 나간 아랫자리를 보정 변수 cc에 정확히 회수해 다음 항에 되돌려 줍니다.

// 카한 보정 합산 (compensated summation)
s ← 0;  c ← 0                // c: 지금까지 잃어버린 아랫자리(부호 반대)
for i = 1 to n do
    y ← x[i] − c             // 지난번에 잃은 만큼 보정한 항
    t ← s + y                // 큰 s에 작은 y를 더하면 y의 아랫자리가 떨어진다
    c ← (t − s) − y          // (t − s)는 실제로 더해진 부분 → 빼면 떨어진 부분
    s ← t
end
return s
정리 1.11 (카한 합산과 짝짓기 합산) 카한 합산의 결과는 ∣S^−S∣≤(2u+O(nu2))∑∣xi∣|\hat S - S| \le \bigl(2u + O(nu^2)\bigr)\sum|x_i|를 만족한다. 곧 nn이 1/u1/u보다 훨씬 작은 한 오차가 nn과 거의 무관하다. 수열을 반으로 나누어 각각 더한 뒤 합치는 짝짓기 합산(pairwise summation)은 각 항이 ⌈log⁡2n⌉\lceil\log_2 n\rceil번만 반올림을 겪어 ∣S^−S∣≤⌈log⁡2n⌉ u∑∣xi∣+O(u2)|\hat S - S| \le \lceil\log_2 n\rceil\,u\sum|x_i| + O(u^2)이다.

보정 합산의 둘째 줄 c←(t−s)−yc \leftarrow (t - s) - y는 수학적으로는 0입니다. 컴파일러가 "대수적으로 단순화"하면 알고리즘이 무너지므로, 실무에서는 공격적인 부동소수점 최적화 옵션을 끄고 씁니다. 비용은 덧셈 네 번으로 재귀 합산의 4배입니다. NumPy의 sum은 짝짓기 합산을, 파이썬의 math.fsum은 이보다 더 정확한 방법을 씁니다.

예제 1.6 (∑1/k2\sum 1/k^2를 단정밀도로) ∑k=1N1/k2\sum_{k=1}^{N} 1/k^2를 단정밀도로 계산한다. 오차는 단정밀도 항들의 정확한 합과의 차이다.
  1. 앞에서부터(큰 항 먼저): N=4097N = 4097부터 1/k21/k^2가 합 1.6447의 반 ulp보다 작아져 합이 1.6447253에서 멈춘다. N=106N = 10^6에서 오차 2.1×10−42.1\times10^{-4}.
  2. 뒤에서부터(작은 항 먼저): N=106N = 10^6에서 오차 8.1×10−88.1\times10^{-8}. 순서만 바꿨는데 2600배 정확하다.
  3. 카한 합산(앞에서부터): N=106N = 10^6에서 3.8×10−83.8\times10^{-8}. 순서와 상관없이 단정밀도로 저장할 수 있는 최선(u⋅1.64≈10−7u \cdot 1.64 \approx 10^{-7})에 가깝다.
SIMULATOR

합산 순서와 카한 합산: 오차–항 수 그래프

앞에서부터뒤에서부터짝짓기카한상한
앞 / 뒤 상대 오차 (N)—
짝짓기 / 카한—
참값 (배정밀도)—
해볼 것: ① '1/k²'에서 빨간 선(앞에서부터)이 N ≈ 4000에서 꺾여 오차가 고정되는 것을 보세요. 합이 멈춘 것입니다. ② '0.1을 반복'에서 앞에서부터 합산의 오차는 대략 N에 비례해 커지고 카한은 10⁻⁸ 근처에 머뭅니다. ③ 상한을 켜면 실제 오차가 상한보다 훨씬 작습니다. 반올림 오차의 부호가 섞여 평균적으로는 √n u 정도로 자라기 때문입니다. 모델: 모든 연산을 Math.fround로 단정밀도 반올림, 참값은 같은 항들을 배정밀도 카한 합산으로 더한 값.

실제 사고: 시간 누적 오차와 변환 오버플로

반올림 오차는 교과서의 장난감이 아닙니다. 공식 조사 보고서로 원인이 확인된 두 사건을 봅시다.

패트리어트 미사일, 1991년 2월 25일

걸프 전쟁 중 사우디아라비아 다란에서 패트리어트 방공 포대가 날아오는 스커드 미사일을 요격하지 못했고, 미군 병사 28명이 숨졌습니다. 미국 회계감사원(GAO) 보고서에 따르면 시스템은 시간을 0.1초 단위 정수로 세고, 이를 24비트 고정소수점 레지스터에 저장된 0.1과 곱해 초로 바꿨습니다. 0.1은 2진법에서 끝나지 않으므로 잘린 값이 저장되었고, 포대는 그날 약 100시간 연속 가동 중이었습니다.

계산해 봅시다. 0.1을 소수점 아래 23비트에서 자르면 ⌊0.1×223⌋/223=0.09999990463…\lfloor 0.1\times2^{23}\rfloor/2^{23} = 0.09999990463\ldots이고, 한 번의 곱마다 오차는 9.54×10−89.54\times10^{-8}초입니다. 100시간은 0.1초 단위로 3.6×1063.6\times10^{6}틱이므로 누적 오차는 3.6×106×9.54×10−8=0.3433.6\times10^6\times9.54\times10^{-8} = 0.343초입니다. 스커드는 초속 약 1.7 km로 날아오므로, 0.34초면 레이더가 미사일을 찾는 위치가 500 m 넘게 어긋납니다. 오차 하나하나는 10−710^{-7}이었지만 오래 쌓이고, 빠른 물체의 위치로 곱해져 치명적이 되었습니다.

아리안 5 첫 비행, 1996년 6월 4일

유럽우주국의 아리안 5 로켓은 첫 발사 후 약 37초 만에 궤도를 벗어나 자폭했습니다. 조사 위원회 보고서에 따르면 관성 기준 장치의 소프트웨어가 64비트 부동소수점 값(수평 속도와 관련된 값)을 16비트 부호 있는 정수로 변환하다가, 값이 3276732767을 넘어 연산 오류가 났습니다. 이 코드는 아리안 4에서 그대로 가져온 것이었는데, 아리안 5는 더 빠르게 가속해 그 값이 아리안 4에서는 나오지 않던 범위까지 커졌습니다. 예비 장치도 같은 소프트웨어여서 같은 순간에 멈췄습니다.

두 사건의 교훈은 다릅니다. 패트리어트는 반올림 오차의 누적, 아리안 5는 표현 범위(오버플로)의 문제였습니다. 둘 다 계산 하나하나는 명세대로 정확했고, 문제는 "이 오차·이 범위가 이 사용 조건에서 괜찮은가"를 묻지 않은 데 있었습니다. 수치해석의 질문이 바로 그것입니다.

SIMULATOR

0.1을 n번: 시계가 얼마나 어긋나는가

틱 수 × 0.1(23비트에서 자름)단정밀도 누적 합배정밀도 누적 합
틱 수 n—
패트리어트식 시간 오차—
초속 1.7 km 물체의 위치 차이—
단정밀도 · 배정밀도 누적 오차—
해볼 것: ① 100시간에서 빨간 선이 0.343초에 닿습니다. 오차는 시간에 정확히 비례합니다(곱셈 한 번의 오차 × 틱 수). ② 단정밀도로 0.1을 반복해 더하면 오차가 훨씬 빠르게 커지다가 들쭉날쭉합니다. 합이 커질수록 ulp가 커져 덧셈마다 잃는 양이 커지기 때문입니다. ③ 배정밀도도 오차가 0은 아니지만 100시간 동안 10⁻⁶초 수준입니다. 모델: 세로축은 |오차|(초)의 로그 눈금, 0.1초마다 한 틱. 실제 시스템의 세부 구현은 단순화했습니다.

이 도구가 쓰이는 곳

반올림 단위 uu와 상쇄·누적의 원리는 이 책의 거의 모든 장에서 다시 나옵니다. 몇 가지 예를 미리 짚어 둡니다.

상황위험처방
실수 비교 x == y반올림으로 같아야 할 값이 다름∣x−y∣≤atol+rtol ∣y∣|x - y| \le \mathrm{atol} + \mathrm{rtol}\,|y|
가까운 두 수의 뺄셈자릿수 상쇄식 변형(유리화, 항등식, 비에트), 전용 함수(log1p, expm1)
긴 합산·내적·평균오차 누적, 작은 항 소실작은 항 먼저, 짝짓기 합산, 카한 합산, 높은 정밀도 누산기
제곱·지수가 들어간 중간값오버플로·언더플로척도 조정(hypot), 로그 영역 계산(log-sum-exp)
시간·돈·카운터0.1 같은 10진 소수의 누적정수 틱으로 세고 마지막에 한 번 변환, 10진 고정소수점

핵심 정리

  1. 수치 계산의 오차는 모델·데이터·절단·반올림 오차로 나뉜다. 이 장은 유한 비트 때문에 생기는 반올림 오차를 다룬다.
  2. IEEE 754 배정밀도는 부호 1, 지수 11(편향 1023), 소수 52비트로 (−1)s(1.f)2 2E−1023(-1)^s(1.f)_2\,2^{E-1023}을 표현한다. 숨은 비트까지 정밀도 p=53p = 53, 10진 약 16자리다. 0.1은 2진법에서 무한 소수라 정확히 저장되지 않는다.
  3. 구간 [2e,2e+1)[2^e, 2^{e+1})에는 2p−12^{p-1}개의 수가 간격 2e−p+12^{e-p+1}로 놓인다. 절대 간격은 크기에 비례하고 상대 간격은 거의 일정하다.
  4. 기계 엡실론 ε=2−52\varepsilon = 2^{-52}, 반올림 단위 u=2−53u = 2^{-53}. 정규 범위에서 fl(x)=x(1+δ)\mathrm{fl}(x) = x(1 + \delta), ∣δ∣≤u|\delta| \le u이고, IEEE 754는 사칙연산과 제곱근에도 같은 보장을 준다.
  5. 1.8×103081.8\times10^{308}을 넘으면 오버플로(∞), 2.2×10−3082.2\times10^{-308}보다 작으면 비정규 수로 점진적 언더플로, 2−10742^{-1074}의 절반 아래는 0. 정의되지 않는 연산은 NaN. 중간값의 범위는 척도 조정으로 지킨다.
  6. 상대 오차는 곱셈·나눗셈에서 거의 더해질 뿐이지만, 뺄셈은 상대 오차를 (∣x∣+∣y∣)/∣x−y∣(|x| + |y|)/|x - y|배 키운다(자릿수 상쇄). 처방은 빼지 않는 식으로 다시 쓰는 것이다.
  7. nn개의 재귀 합산 오차는 (n−1)u∑∣xi∣(n - 1)u\sum|x_i| 이하다. 작은 항 먼저, 짝짓기(log⁡2n\log_2 n), 카한 보정 합산(2u2u)으로 줄인다.

확인 퀴즈

Q1. 배정밀도에서 0.1 + 0.2 == 0.3이 거짓인 근본 이유는?

세 수 모두 2진 무한 소수라 가장 가까운 double로 저장된다. fl(0.1) + fl(0.2)를 올바르게 반올림한 결과는 fl(0.3)의 바로 옆 수(한 ulp 위)다. 덧셈 자체는 IEEE 754 규칙대로 정확히 반올림되었다.

Q2. 배정밀도 수 1024의 ulp(다음 수까지의 간격)는?

1024 = 2¹⁰이므로 구간 [2¹⁰, 2¹¹)의 간격 2^(e−p+1) = 2^(10−52) = 2⁻⁴². 일반적으로 ulp(2ᵉ) = ε·2ᵉ이다.

Q3. 정밀도 p = 24인 단정밀도의 반올림 단위 u는?

u = 2^(−p) = 2⁻²⁴. 기계 엡실론은 그 두 배인 ε = 2^(1−p) = 2⁻²³이다. 2⁻¹²⁶은 단정밀도의 최소 정규 수다.

Q4. 배정밀도에서 1e16 + 1 − 1e16의 결과는?

10¹⁶ 근처의 ulp는 2이다. 10¹⁶ + 1은 두 이웃 10¹⁶과 10¹⁶ + 2의 정확히 한가운데라 짝수 쪽인 10¹⁶으로 반올림된다. 따라서 결과는 0. 더하는 순서를 1e16 − 1e16 + 1로 바꾸면 1이 나온다.

Q5. x와 y가 각각 상대 오차 10⁻¹⁶을 가지고 앞 12자리가 같다. x − y의 상대 오차는 대략 얼마까지 커질 수 있는가?

앞 12자리가 같으면 |x − y| ≈ 10⁻¹²|x|이고 증폭 인자 (|x|+|y|)/|x−y| ≈ 2×10¹². 10⁻¹⁶ × 2×10¹² ≈ 2×10⁻⁴. 16자리 중 12자리를 잃어 4자리 남짓 남는다.

Q6. x² − 10⁶x + 1 = 0의 작은 근을 정확히 구하는 방법은?

두 근의 곱이 c/a = 1(비에트)이므로, 같은 부호끼리 더하는 큰 근은 상쇄가 없고 작은 근은 나눗셈으로 얻는다. 정밀도를 높이면 문제를 늦출 뿐 b가 더 커지면 다시 생긴다.

Q7. 0 < x₁ < x₂ < … < xₙ인 양수들을 재귀 합산할 때 오차가 대체로 더 작은 순서는?

먼저 더한 항일수록 더 많은 반올림(최대 n−1번)을 겪는다. 작은 항을 먼저 더하면 그 많은 반올림이 작은 값에 곱해지고, 작은 항이 큰 합에 먹혀 사라지는 일도 줄어든다. 예제 1.6에서 2600배 차이가 났다.

Q8. 카한 합산에서 보정 변수 c ← (t − s) − y가 하는 일은?

t − s는 실제로 s에 더해진 부분이고, 여기서 y를 빼면 반올림으로 잃은 부분(부호 반대)이 남는다. 수학적으로는 0이지만 부동소수점에서는 0이 아니며, 다음 항에 되돌려져 오차가 n에 거의 무관한 2u 수준으로 유지된다.