계산기에 0.1 + 0.2를 넣으면 0.3이 나오지만, 대부분의 프로그래밍 언어는 0.30000000000000004를 돌려줍니다. 버그가 아닙니다. 컴퓨터는 실수를 유한한 비트로 저장하고, 모든 연산 결과를 그 유한한 집합의 가장 가까운 원소로 반올림합니다. 수치해석은 바로 이 사실에서 출발합니다. 이 책 전체의 관점은 하나입니다. 컴퓨터는 유한한 수와 유한한 단계로 근사한다. 좋은 수치해석은 그 오차를 예측하고, 통제하고, 비용과 바꾸는 기술이다. 이 장은 그 첫 번째 오차, 곧 수를 저장하는 순간 생기는 반올림 오차를 해부합니다. 64개의 비트가 어떻게 수가 되는지, 표현할 수 있는 수가 수직선 위에 어떻게 흩어져 있는지, 단 하나의 상수 u≈1.1×10−16이 모든 연산의 오차를 어떻게 묶어 주는지, 그리고 그 작은 오차가 뺄셈과 긴 합산에서 어떻게 치명적으로 커지는지 차례로 봅니다.
정수·고정소수점 표현의 한계를 설명하고, 부동소수점 수 체계 F(β,p,emin,emax)를 정의한다.
IEEE 754 배정밀도 수의 부호·지수·가수 비트를 읽고 쓰며, 0.1이 왜 정확히 저장되지 않는지 설명한다.
부동소수점 수의 간격이 크기에 비례한다는 사실과 기계 엡실론 ε, 반올림 단위 u, ulp의 관계를 안다.
오버플로·언더플로·비정규 수·Inf·NaN이 언제 생기고 어떻게 피하는지 안다.
절대·상대 오차와 유효 숫자를 계산하고, 자릿수 상쇄가 상대 오차를 (∣x∣+∣y∣)/∣x−y∣배 키운다는 것을 증명한다.
1970년대까지 컴퓨터 회사마다 실수를 저장하는 형식과 반올림 규칙이 달랐습니다. 같은 프로그램이 기계에 따라 다른 답을 냈고, 어떤 기계에서는 x=y인데 x−y=0이 되기도 했습니다. 수치 소프트웨어를 만드는 사람은 기계마다 오차 분석을 다시 해야 했습니다. 1985년에 제정된 IEEE 754 표준은 수의 형식과 함께 "사칙연산과 제곱근의 결과는 정확한 값을 올바르게 반올림한 것이어야 한다"는 규칙을 정했습니다. 이 표준을 이끈 카한(William Kahan)은 1989년 튜링상을 받았습니다.
이 한 줄의 규칙 덕분에 모든 연산의 오차를 fl(x∘y)=(x∘y)(1+δ), ∣δ∣≤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.1에 2를 거듭 곱하며 정수 부분을 떼어 내면 2진 자리가 하나씩 나옵니다.
예제 1.1 (0.1의 2진 전개) 0.1에 2를 곱하고 정수 부분을 다음 자리로 떼어 내는 일을 되풀이하라.
같은 방식으로 fl(0.2)=0.2000000000000000111…, fl(0.3)=0.2999999999999999888…이다. fl(0.1)+fl(0.2)의 정확한 합 0.3000000000000000166…은 이웃한 두 double의 정확히 한가운데라, 마지막 비트가 짝수인 쪽으로 반올림되어 0.3000000000000000444…이 되고, 이는 fl(0.3)과 바로 옆의 다른 수다. 둘의 차이는 5.55×10−17, 곧 이 근처 수 간격(ulp) 한 칸이다.
이 현상은 프로그래밍 언어의 결함이 아니라 유한한 비트로 실수를 다루는 모든 계산의 성질입니다. 그리고 수치해석이 다루는 오차 가운데 하나일 뿐입니다. 실제 계산의 답이 참값과 다른 이유는 크게 네 가지입니다.
그림 1-1. 계산 결과가 참값과 다른 네 가지 이유. 모델 오차와 데이터 오차는 수치해석 이전의 문제이고, 수치해석은 절단 오차(무한 과정을 유한하게 자름)와 반올림 오차(실수를 유한 비트로 저장함), 그리고 앞선 오차가 계산을 거치며 커지거나 줄어드는 전파를 다룬다. 2장의 조건수가 전파를 재는 도구다.
정의 1.1 (절단 오차와 반올림 오차) 무한 과정(급수, 극한, 적분, 미분)을 유한한 단계로 대신해서 생기는 오차를 절단 오차(truncation error)라 한다. 예: ex≈∑k=0nxk/k!, f′(x)≈(f(x+h)−f(x))/h. 실수를 유한한 자릿수의 수로 저장하거나 연산 결과를 유한 자릿수로 줄여서 생기는 오차를 반올림 오차(rounding error)라 한다.
절단 오차는 단계 수를 늘리거나 간격 h를 줄이면 작아집니다. 3장부터 16장까지 이 책의 대부분은 절단 오차를 O(hp)로 재고 줄이는 이야기입니다. 반올림 오차는 반대로, 단계를 늘릴수록 쌓이고 h를 줄일수록 커지기도 합니다(9장의 V자 그래프). 두 오차의 균형을 잡으려면 먼저 반올림 오차의 크기를 정확히 알아야 합니다. MathBook의 컴퓨터는 어떻게 틀리는가에서 맛본 내용을 여기서 엄밀하게 다시 세웁니다.
정수와 고정소수점의 한계
원자의 질량과 별의 질량을 같은 변수에
전자의 질량은 9.1×10−31 kg, 태양의 질량은 2.0×1030 kg입니다. 한 시뮬레이션 안에서 두 값이 모두 나올 수 있습니다. 소수점 위치를 고정한 표현으로 둘을 모두 담으려면 몇 자리가 필요할까요?
가장 단순한 표현은 정수입니다. 32비트 부호 있는 정수(2의 보수)는 −231부터 231−1=2147483647까지를 간격 1로 빠짐없이 담습니다. 이 범위 안의 덧셈·곱셈은 정확합니다. 그러나 범위를 넘으면 경고 없이 값이 돌아갑니다. 2147483647+1은 32비트 연산에서 −2147483648이 됩니다(자바스크립트로 (2147483647 + 1) | 0).
소수를 담으려면 고정소수점(fixed point) 표현을 씁니다. 32비트 중 아래 16비트를 소수 부분으로 정하면(Q16.16 형식) 저장된 정수 N이 값 N/216을 뜻합니다. 간격은 2−16≈1.5×10−5로 일정하고, 범위는 ±32768입니다. 신호 처리 칩이나 부동소수점 장치가 없는 마이크로컨트롤러는 지금도 이 방식을 씁니다. 하지만 간격이 절대적으로 고정되어 있다는 것이 치명적입니다.
0.00001을 저장하면 가장 가까운 값은 1/65536=0.0000153이라 상대 오차가 50%를 넘는다.
30000을 저장하면 상대 오차는 10−10 수준으로 지나치게 정확하다.
10−31과 1030을 함께 담으려면 소수점 앞뒤로 각각 100비트가 넘게 필요하다.
과학자들이 오래전부터 쓰는 해법은 과학적 표기법입니다. 9.1×10−31처럼 유효 숫자 몇 자리와 지수를 따로 적으면, 크기와 상관없이 상대 정밀도가 일정합니다. 컴퓨터의 부동소수점은 이것을 2진법으로 옮긴 것입니다. 소수점이 지수에 따라 "떠다닌다"(floating)고 해서 붙은 이름입니다.
그림 1-2. 같은 개수의 수를 고정소수점은 고르게, 부동소수점은 2의 거듭제곱 구간마다 같은 개수씩(여기서는 4개) 놓는다. 그래서 부동소수점의 상대 간격은 크기와 거의 무관하다.
IEEE 754 배정밀도: 부호·지수·가수
정의 1.2 (부동소수점 수 체계) 밑 β, 정밀도 p, 지수 범위 [emin,emax]의 부동소수점 수(floating-point number)는
1023을 지수 편향(exponent bias)이라 한다. 편향을 쓰면 지수 비트를 부호 없는 정수로 비교할 수 있어, 양수끼리의 크기 비교가 64비트 정수 비교와 같아진다.
예제 1.2 (−2.5와 0.1을 비트로)
−2.5=−10.12=−1.012×21. 부호 s=1, 지수 E=1+1023=1024=100000000002, 소수 f=0100…0. 16진수로 c004000000000000.
0.1=1.100110011001…2×2−4. E=−4+1023=1019=011111110112. 소수 52비트는 1001 반복이고, 53번째 이후 비트가 1001…(절반보다 큼)이라 마지막 자리를 올린다: …100110011010. 16진수로 3fb999999999999a. 끝의 a(1010)가 올림의 흔적이다.
1=1.02×20은 E=1023, f=0, 3ff0000000000000.
형식
비트
지수 비트
정밀도 p
ε = 21−p
최댓값
최소 정규 수
반정밀도 binary16
16
5
11
9.77×10⁻⁴
65504
6.10×10⁻⁵
bfloat16 (IEEE 아님)
16
8
8
7.81×10⁻³
3.39×10³⁸
1.18×10⁻³⁸
단정밀도 binary32
32
8
24
1.19×10⁻⁷
3.40×10³⁸
1.18×10⁻³⁸
배정밀도 binary64
64
11
53
2.22×10⁻¹⁶
1.80×10³⁰⁸
2.23×10⁻³⁰⁸
4배정밀도 binary128
128
15
113
1.93×10⁻³⁴
1.19×10⁴⁹³²
3.36×10⁻⁴⁹³²
배정밀도의 53비트는 10진법으로 53log102≈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) 안의 수들은 가수의 마지막 자리 한 칸씩 떨어져 있습니다. 지수가 하나 커지면 같은 개수의 수가 두 배 넓은 구간에 퍼집니다.
정리 1.4 (부동소수점 수의 간격)F(2,p,emin,emax)에서 emin≤e≤emax일 때 구간 [2e,2e+1) 안에는 정확히 2p−1개의 수
2e,2e+2e−p+1,2e+2⋅2e−p+1,…가 간격 2e−p+1로 놓인다. 따라서 이 구간의 수 x와 다음 수의 간격을 ∣x∣로 나눈 상대 간격은
2−p<∣x∣2e−p+1≤21−p
로, 크기와 무관하게 두 배 범위 안에 있다.
증명. 정규화된 가수 1.f1…fp−1은 p−1개의 비트로 2p−1가지 값을 가지며, 이웃한 가수의 차는 2−(p−1)이다. 여기에 2e를 곱하면 간격 2e−p+1이다. 2e≤∣x∣<2e+1로 나누면 상대 간격의 범위가 나온다. ∎
배정밀도에서 1 다음 수는 1+2−52, 1000 근처의 간격은 1.14×10−13, 1016 근처의 간격은 2입니다. 그래서 1016+1은 1016으로 반올림되어 사라집니다. 정수를 빠짐없이 담는 한계는 253=9007199254740992이고, 253+1은 double로 표현할 수 없습니다. 자바스크립트의 Number.MAX_SAFE_INTEGER가 253−1인 이유입니다.
아래 시뮬레이터는 정밀도 p와 지수 범위를 작게 줄인 "장난감 부동소수점 체계"를 수직선에 그립니다. 64비트 체계도 정확히 같은 모양이고, 단지 눈금이 252배 촘촘할 뿐입니다.
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−20으로 두면 어떻게 될까요? x≈1인 근처에서 두 double의 차이는 0이 아니면 최소 2.2×10−16이므로 이 조건은 xnew=x일 때만 참이 됩니다. 컴퓨터가 구별할 수 있는 상대 차이의 한계를 알아야 합니다.
정의 1.5 (기계 엡실론, 반올림 단위, ulp)
기계 엡실론(machine epsilon)ε은 1과 1 다음의 부동소수점 수 사이의 간격이다. F(2,p,⋅,⋅)에서 ε=21−p, 배정밀도에서 ε=2−52≈2.22×10−16.
부동소수점 수 x의 ulp(unit in the last place)는 x가 속한 구간 [2e,2e+1)의 간격 2e−p+1=ε⋅2e이다.
IEEE 754의 기본 반올림은 가장 가까운 수로, 동률이면 마지막 비트가 0인(짝수) 쪽으로(round to nearest, ties to even)입니다. 동률을 늘 위로 올리면 긴 계산에서 오차가 한쪽으로 쏠리기 때문에, 짝수 쪽을 골라 평균적으로 상쇄되게 한 것입니다. 이 반올림에서 오차 상한은 간격의 절반입니다.
정리 1.6 (반올림 오차의 상한)x가 정규 수 범위 안의 실수이고 fl(x)가 가장 가까운 부동소수점 수로의 반올림이면
fl(x)=x(1+δ),∣δ∣≤u=2−p.
증명.2e≤∣x∣<2e+1이라 하자. 정리 1.4에 따라 이 구간의 간격은 2e−p+1이므로 가장 가까운 수까지의 거리는 그 절반 2e−p 이하다. 따라서 ∣fl(x)−x∣/∣x∣≤2e−p/2e=2−p. δ=(fl(x)−x)/x로 두면 된다. ∎
이 정리는 수 하나를 저장할 때의 이야기입니다. IEEE 754는 여기에 결정적인 약속을 더합니다. +,−,×,÷,의 결과는 정확한 결과를 반올림한 값이어야 합니다(correctly rounded). 그러므로 부동소수점 수 x,y에 대해
fl(x∘y)=(x∘y)(1+δ),∣δ∣≤u,∘∈{+,−,×,÷}.
이것이 표준 반올림 오차 모형이다. 2장에서 이 식 하나로 합산·내적·다항식 계산의 오차를 분석한다. 주의: δ는 연산마다 다른, 크기만 아는 수다.
기계 엡실론은 실험으로 찾을 수 있습니다. 1에 e를 더해 1보다 커지는지 확인하며 e를 반씩 줄이면 됩니다.
// 기계 엡실론 찾기: 1 + e/2 가 1과 구별되지 않을 때까지 e를 반씩 줄인다
e ← 1
while fl(1 + e/2) > 1 do
e ← e / 2
endreturn e // double: 52번 반복 후 e = 2^(-52)
예제 1.3 (엡실론 찾기의 마지막 세 단계) 배정밀도에서 위 반복을 따라가라.
50번 반복 후 e=2−50: fl(1+2−51)=1.0000000000000004>1이므로 계속.
51번 후 e=2−51: fl(1+2−52)=1.0000000000000002>1이므로 계속.
52번 후 e=2−52: 1+2−53은 1과 1+2−52의 정확히 한가운데다. 동률이므로 마지막 비트가 0인 1로 반올림되어 fl(1+2−53)=1. 멈춘다. 결과 e=2−52=2.220446049250313×10−16, 자바스크립트의 Number.EPSILON과 같다.
SIMULATOR
기계 엡실론 찾기: 1이 움직이지 않을 때까지 반으로
형식
k
e
fl(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×10200과 4×10200인 직각삼각형의 빗변은 5×10200으로 double 범위 안에 넉넉히 들어갑니다. 그런데 Math.sqrt(x*x + y*y)는 Infinity를 돌려줍니다. 중간값 x2=9×10400이 범위를 넘었기 때문입니다. 답이 아니라 중간 계산이 범위를 벗어나는 일을 어떻게 막을까요?
지수의 범위가 유한하므로 표현할 수 있는 크기에도 위아래 끝이 있습니다.
정의 1.7 (오버플로, 언더플로, 비정규 수) 결과의 크기가 최댓값 xmax=(2−2−52)×21023≈1.80×10308을 넘으면 오버플로(overflow)이고 결과는 ±∞가 된다. 크기가 최소 정규 수 xmin=2−1022≈2.23×10−308보다 작으면 언더플로(underflow)다. 이때 IEEE 754는 0으로 바로 떨어뜨리지 않고, 지수를 −1022에 고정한 채 첫 자리를 0으로 허용하는 비정규 수(subnormal number)(0.f)2×2−1022를 쓴다. 가장 작은 양의 비정규 수는 2−1074≈4.94×10−324이다.
비정규 수는 0과 xmin 사이를 간격 2−1074로 고르게 채웁니다(그림의 주황 눈금). 이것을 점진적 언더플로(gradual underflow)라 합니다. 덕분에 "x=y이면 x−y=0"이 언제나 성립합니다. 대가는 정밀도입니다. 비정규 수에서는 앞자리 0들이 가수를 차지하므로 상대 오차 상한 u가 성립하지 않고, 2−1074 근처의 수는 유효 숫자가 한 자리뿐입니다.
연산이 수학적으로 정의되지 않으면 결과는 NaN(Not a Number)입니다. 특수 값의 규칙은 대부분 극한의 상식을 따릅니다.
연산
결과
설명
1e308 × 10
Infinity
오버플로
1 / 0, −1 / 0
Infinity, −Infinity
0으로 나누기는 오류가 아니라 부호 있는 무한대
1 / −0
−Infinity
−0은 0과 같다고 비교되지만(−0 == 0) 부호를 기억한다
0 / 0, ∞ − ∞, 0 × ∞, √−1
NaN
정의되지 않는 연산
NaN == NaN
false
NaN은 자기 자신과도 같지 않다(x != x로 검사)
2⁻¹⁰²² / 2
1.11×10⁻³⁰⁸
비정규 수(점진적 언더플로)
2⁻¹⁰⁷⁴ / 2
0
완전한 언더플로(동률 → 짝수인 0)
NaN과 Inf는 "계산을 멈추지 않고 문제를 끝까지 운반"하도록 설계되었습니다. 계산 도중 한 번 NaN이 생기면 이후 거의 모든 연산 결과가 NaN이 되므로, 마지막에 한 번만 검사하면 됩니다. 반대로 말하면 NaN이 나왔을 때는 처음 생긴 곳을 찾아야 합니다.
빗변 문제의 처방은 척도 조정(scaling)입니다. 큰 쪽 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 ≤ 1return 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=0의 근삿값 x^에 대해 절대 오차(absolute error)는 ∣x^−x∣, 상대 오차(relative error)는 ∣x^−x∣/∣x∣이다. 상대 오차가 5×10−q 이하인 가장 큰 정수 q를 x^의 (10진) 유효 숫자(significant digits) 개수라 한다. 대략 q≈−log10(상대오차)이다.
예제 1.4 (원주율의 두 근삿값)π=3.14159265…
22/7=3.142857…: 절대 오차 1.264×10−3, 상대 오차 4.02×10−4≤5×10−4. 유효 숫자 3자리(3.14).
355/113=3.1415929…: 절대 오차 2.67×10−7, 상대 오차 8.49×10−8≤5×10−7. 유효 숫자 7자리.
배정밀도의 Math.PI는 상대 오차 u 이하, 유효 숫자 약 16자리다. 정리 1.6은 "부동소수점에 저장하면 유효 숫자 약 16자리가 보장된다"는 뜻이다.
참값이 0에 가까우면 상대 오차는 쓸 수 없으므로 절대 오차를 씁니다. 실무의 멈춤 조건이 흔히 ∣x^−x∣≤atol+rtol⋅∣x∣처럼 둘을 섞는 이유입니다(3장, 11장). 상대 오차의 중요한 성질은 곱셈과 나눗셈에서 거의 더해진다는 것입니다. x^=x(1+δ1), y^=y(1+δ2)이면 x^y^=xy(1+δ1+δ2+δ1δ2). 곱셈·나눗셈은 상대 오차를 키우지 않습니다. 문제는 덧셈과 뺄셈입니다.
자릿수 상쇄: 비슷한 두 수의 뺄셈
근의 공식이 틀린 답을 낸다
x2−108x+1=0의 두 근은 약 108과 10−8입니다. 근의 공식 x=(108±1016−4)/2를 그대로 double로 계산하면 작은 근이 7.45×10−9으로 나옵니다. 상대 오차 25%, 유효 숫자가 하나도 맞지 않습니다. 연산 하나하나의 오차는 10−16인데 어떻게 이런 일이 생길까요?
뺄셈 자체는 정확합니다. 아주 가까운 두 부동소수점 수의 차는 반올림 없이 정확히 계산됩니다(스테르베즈 보조정리(Sterbenz lemma): y/2≤x≤2y이면 x−y는 부동소수점 수다). 문제는 뺄셈에 들어가는 값이 이미 오차를 품고 있다는 것입니다. 앞자리가 같은 두 수를 빼면 같은 앞자리가 사라지고, 오차가 섞인 뒷자리만 남아 앞으로 올라옵니다. 이것을 자릿수 상쇄(catastrophic cancellation)라 합니다.
정리 1.9 (뺄셈의 오차 증폭)x^=x(1+δx), y^=y(1+δy), ∣δx∣,∣δy∣≤δ이고 x=y이면, 뺄셈을 정확히 하더라도
증폭 인자 (∣x∣+∣y∣)/∣x−y∣는 x≈y일 때 한없이 커진다. 예를 들어 x와 y가 앞 10자리가 같으면 인자는 약 2×1010이고, 16자리 중 10자리를 잃어 6자리만 남는다. (이 인자는 2장에서 뺄셈의 조건수로 다시 나온다.)
근의 공식에서는 −b=108과 b2−4ac=99999999.99999998이 앞 16자리가 거의 같습니다. 의 결과에 있던 10−16 수준의 상대 오차가 108/10−8배 증폭되어 결과를 망칩니다. 처방은 빼지 않는 식으로 바꾸는 것입니다. 두 근의 곱이 c/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=0의 작은 근) 근의 공식 (b−b2−4)/2와 위 알고리즘 2/(b+b2−4)를 double로 비교한다.
b
근의 공식 그대로
상대 오차
상쇄 없는 식
상대 오차
10²
0.010001000200048793
1.2×10⁻¹³
0.010001000200050014
< 10⁻¹⁶
10⁴
0.00010000000111176632
1.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⁻¹⁶
표의 둘째 열은 b가 100배 커질 때마다 오차가 약 104배 커집니다. 증폭 인자 ≈b2과 맞습니다. 같은 일이 1−cosx에서도 일어납니다. x=10−8이면 cosx=1−5×10−17인데 이 값은 1과 u보다 가까워 fl(cosx)=1이고, 결과는 0입니다. 참값은 5×10−17입니다. 삼각 항등식 1−cosx=2sin2(x/2)는 뺄셈이 없어 모든 x에서 정확합니다.
x
10⁻²
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+21+31+⋯는 발산합니다. 그런데 단정밀도로 차례대로 더하면 합이 15.403683에서 멈춥니다. k=2097152번째 항 1/k≈4.8×10−7이 합 15.4의 ulp의 절반보다 작아져서, 더해도 합이 바뀌지 않기 때문입니다. 긴 합산에서는 덧셈마다 생기는 작은 오차와 "작은 수가 큰 수에 먹히는" 현상이 쌓입니다.
n개의 수를 차례대로 더하는 재귀 합산s1=x1, sk=fl(sk−1+xk)의 오차를 표준 모형으로 추적해 봅시다. 각 덧셈에서 sk=(sk−1+xk)(1+δk), ∣δk∣≤u입니다.
정리 1.10 (재귀 합산의 오차 한계) 위 재귀 합산의 결과 S^=sn과 참값 S=∑xi에 대해
∣S^−S∣≤(n−1)ui=1∑n∣xi∣+O(u2).
증명의 핵심. 식을 펼치면 S^=x1∏k=2n(1+δk)+x2∏k=2n(1+δk)+x3∏k=3n(1+δk)+⋯+xn(1+δn). 항 xi는 n−i+1번(x1은 n−1번) 반올림을 겪는다. ∏(1+δk)=1+θ, ∣θ∣≤mu+O(u2)을 쓰면 ∣S^−S∣≤∑i(n−i+1)u∣xi∣≤(n−1)u∑∣xi∣ (첫 항 보정 포함). ∎
이 정리에서 두 가지 교훈이 나옵니다. 첫째, 먼저 더한 항일수록 더 많은 반올림을 겪으므로, 크기가 작은 항부터 더하면 큰 m이 작은 ∣xi∣에 곱해져 실제 오차가 줄어듭니다. 둘째, 상한이 ∑∣xi∣에 비례하므로, 부호가 섞여 ∣S∣≪∑∣xi∣인 합(교대 급수)은 상대 오차가 클 수 있습니다. 뺄셈 상쇄의 합산판입니다.
순서를 바꾸는 것보다 강력한 방법은 잃어버린 부분을 따로 모으는 것입니다. 카한(Kahan)이 1965년에 발표한 보정 합산은 덧셈마다 반올림으로 떨어져 나간 아랫자리를 보정 변수 c에 정확히 회수해 다음 항에 되돌려 줍니다.
// 카한 보정 합산 (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
endreturn s
정리 1.11 (카한 합산과 짝짓기 합산) 카한 합산의 결과는 ∣S^−S∣≤(2u+O(nu2))∑∣xi∣를 만족한다. 곧 n이 1/u보다 훨씬 작은 한 오차가 n과 거의 무관하다. 수열을 반으로 나누어 각각 더한 뒤 합치는 짝짓기 합산(pairwise summation)은 각 항이 ⌈log2n⌉번만 반올림을 겪어 ∣S^−S∣≤⌈log2n⌉u∑∣xi∣+O(u2)이다.
보정 합산의 둘째 줄 c←(t−s)−y는 수학적으로는 0입니다. 컴파일러가 "대수적으로 단순화"하면 알고리즘이 무너지므로, 실무에서는 공격적인 부동소수점 최적화 옵션을 끄고 씁니다. 비용은 덧셈 네 번으로 재귀 합산의 4배입니다. NumPy의 sum은 짝짓기 합산을, 파이썬의 math.fsum은 이보다 더 정확한 방법을 씁니다.
카한 합산(앞에서부터): N=106에서 3.8×10−8. 순서와 상관없이 단정밀도로 저장할 수 있는 최선(u⋅1.64≈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…이고, 한 번의 곱마다 오차는 9.54×10−8초입니다. 100시간은 0.1초 단위로 3.6×106틱이므로 누적 오차는 3.6×106×9.54×10−8=0.343초입니다. 스커드는 초속 약 1.7 km로 날아오므로, 0.34초면 레이더가 미사일을 찾는 위치가 500 m 넘게 어긋납니다. 오차 하나하나는 10−7이었지만 오래 쌓이고, 빠른 물체의 위치로 곱해져 치명적이 되었습니다.
아리안 5 첫 비행, 1996년 6월 4일
유럽우주국의 아리안 5 로켓은 첫 발사 후 약 37초 만에 궤도를 벗어나 자폭했습니다. 조사 위원회 보고서에 따르면 관성 기준 장치의 소프트웨어가 64비트 부동소수점 값(수평 속도와 관련된 값)을 16비트 부호 있는 정수로 변환하다가, 값이 32767을 넘어 연산 오류가 났습니다. 이 코드는 아리안 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초마다 한 틱. 실제 시스템의 세부 구현은 단순화했습니다.
이 도구가 쓰이는 곳
반올림 단위 u와 상쇄·누적의 원리는 이 책의 거의 모든 장에서 다시 나옵니다. 몇 가지 예를 미리 짚어 둡니다.