소수점이 있는 수를 정해진 칸 수에 담는 방법이다. 과학 시간에 큰 수를 “1.23 곱하기 10의 몇 제곱”으로 적듯이, 앞자리 숫자 몇 개와 소수점을 몇 칸 옮길지를 따로 적는다. 덕분에 아주 작은 수부터 아주 큰 수까지 같은 칸 수로 적는다. 대신 적을 수 있는 수가 띄엄띄엄 놓이고 그 간격이 수가 커질수록 벌어져서, 거의 모든 계산 결과는 가장 가까운 칸으로 살짝 옮겨진 값이다.
예시로 보기
정수만으로는 4.5나 0.1을 적을 수 없다. 가장 쉬운 해결은 소수점 자리를 못 박아 두는 것이다. 2진수에서 소수점 아래 칸의 무게는 \(\frac12, \frac14, \frac18, \dots\)이다. 예로 0.1011을 읽어 본다.
| 칸 | 1 | 0 | 1 | 1 |
|---|---|---|---|---|
| 무게 | \(2^{-1} = 0.5\) | \(2^{-2} = 0.25\) | \(2^{-3} = 0.125\) | \(2^{-4} = 0.0625\) |
\(0.5 + 0.125 + 0.0625 = 0.6875\)다. 이렇게 소수점 자리를 고정하는 방식이 고정소수점이다.
고정소수점은 범위가 좁다. 소수점 아래에 4칸을 주면 0.0625보다 작은 차이는 적지 못하고, 위쪽 칸이 모자라면 큰 수도 못 적는다. 은하의 지름과 원자의 지름을 같은 형식에 담을 수 없다.
그래서 소수점을 띄운다. 10진수로 먼저 보면, 12.345는 이렇게 쪼갤 수 있다.
\[12.345 = \underbrace{12345}_{\text{앞자리 숫자들}} \times \underbrace{10}_{\text{밑}}{}^{\overbrace{-3}^{\text{지수}}}\]숫자 덩어리 12345와 “소수점을 왼쪽으로 3칸”이라는 −3을 따로 적는다. 소수점이 지수에 따라 떠다닌다고 해서 부동(浮動)소수점이다. 2진수에서는 밑이 2다.
32비트 예를 하나 읽어 본다.
부호 지수 (8비트) 가수 (23비트)
┌─┐ ┌────────┐ ┌───────────────────────┐
0 01111100 01000000000000000000000
└─┘ └────────┘ └───────────────────────┘
31 30 ... 23 22 ................... 0 (비트 번호)
- 부호: 0이라 양수다.
- 지수:
01111100은 124다. 여기서 127을 뺀 −3이 진짜 지수다. - 가수: 맨 앞에 “1.”이 숨어 있다고 본다.
01000…은 \(0.25\)라 \(1.25\)다. - 합친다: \(+1.25 \times 2^{-3} = 0.15625\).
0.1011 = 0.6875, 4.50 + 2.75와 4.50 × 2.75의 2진 계산, 32비트 예 0.15625, 단정도의 끝값 네 개, 3FD5 5555 5555 5555 ≈ 1/3, \(2^{53}\)까지의 정수, 간격이 벌어지는 것, 7.22자리와 15.95자리, 카드의 값 — 03_floating-point_verify.py정의
IEEE 754는 부동소수점을 비트에 담는 방법을 정한 표준이다. 비트를 세 토막으로 나눈다.
| 전체 | 부호 | 지수 | 가수 | 지수에서 빼는 값 | |
|---|---|---|---|---|---|
단정도 (C의 float) |
32비트 | 1 | 8 | 23 | 127 |
배정도 (C의 double) |
64비트 | 1 | 11 | 52 | 1023 |
부호 비트를 \(s\), 지수 토막을 부호 없는 정수로 읽은 값을 \(E\), 가수 토막의 비트를 왼쪽부터 \(b_1 b_2 \cdots\)라 하자. 단정도의 값은 다음과 같다.
\[\text{value} = (-1)^{s} \times 2^{E - 127} \times \left(1 + \sum_{i=1}^{23} b_i 2^{-i}\right)\]말로 읽으면 “부호, 곱하기 2의 (\(E\) 빼기 127)제곱, 곱하기 1.가수”다. 배정도는 127 대신 1023, 23 대신 52를 쓴다.
- 지수에서 127을 빼는 까닭. 지수 토막은 부호 없는 정수라 음수를 못 적는다. 127을 빼 주면 1~254가 −126~127이 되어 작은 수와 큰 수를 고루 적는다. 0과 255는 특별한 값에 쓴다1.
- 맨 앞의 1을 적지 않는 까닭. 2진수에서 0이 아닌 수의 맨 앞 숫자는 늘 1이다. 늘 같은 것은 적지 않아도 되니 1비트를 아낀다. 그래서 가수 23비트로 유효 숫자 24비트를 적는다1.
표현 범위. 단정도에서 “1.가수”는 1부터 \(2 - 2^{-23} \approx 1.999\,999\,881\)까지, \(2^{E-127}\)은 \(2^{-126} \approx 1.175 \times 10^{-38}\)부터 \(2^{127} \approx 1.701 \times 10^{38}\)까지다. 그래서 가장 큰 수는 약 \(3.40 \times 10^{38}\)이다. 배정도는 약 \(1.80 \times 10^{308}\)까지다.
정밀도. 유효 숫자가 2진수로 24자리(단정도), 53자리(배정도)다. 10진수로 바꾸면 약 7.22자리와 15.95자리다. float는 일곱 자리쯤, double은 열여섯 자리쯤 믿을 수 있다는 뜻이다.
간격은 수가 클수록 벌어진다. 유효 숫자의 칸 수가 정해져 있으니, 지수가 1 커질 때마다 이웃한 두 수의 간격이 두 배가 된다. 가로축에 수의 크기를, 세로축에 그 근처의 간격을 두고 둘 다 로그 눈금으로 그리면 곧은 선으로 올라간다. 배정도의 선은 단정도보다 아래에 놓인다.
| 수의 크기 | 단정도의 간격 | 배정도의 간격 |
|---|---|---|
| 1 근처 | \(2^{-23} \approx 1.2 \times 10^{-7}\) | \(2^{-52} \approx 2.2 \times 10^{-16}\) |
| \(2^{24} \approx 1{,}678\)만 근처 | 2 | \(2^{-28}\) |
| \(2^{53} \approx 9 \times 10^{15}\) 근처 | \(2^{30}\) | 2 |
배정도는 \(-2^{53}\)부터 \(2^{53}\)까지의 정수를 하나도 빠짐없이 적는다. 그 위로 \(2^{54}\)까지는 짝수만, 그다음은 4의 배수만 적는다.
예제
2진 소수의 덧셈과 곱셈. \(4.50 + 2.75\)와 \(4.50 \times 2.75\)를 2진수로 계산한다.
- 2진수로 적는다: \(4.5 = 100.1_2\), \(2.75 = 10.11_2\).
- 더한다: 소수점을 맞춰
100.10+010.11=111.01. \(4 + 2 + 1 + 0.25 = 7.25\)다. - 곱한다: 소수점을 떼고 \(1001_2 \times 1011_2 = 9 \times 11 = 99 = 1100011_2\). 소수점 아래 자리가 1칸과 2칸이었으니 합쳐 3칸을 되돌려 \(1100.011_2 = 12.375\)다.
10진수 필산과 방법이 같다1.
배정도 한 개 읽기. 16진수 3FD5 5555 5555 5555를 읽어 본다.
- 토막 내기: 맨 앞 3글자
3FD가 부호와 지수 12비트다. 부호 0, 지수 \(\text{3FD}_{16} = 1021\). - 진짜 지수: \(1021 - 1023 = -2\).
- 가수:
5555…는0101 0101 …이라 “1.0101…” \(= 1.333\ldots\)이다. - 합친다: \(1.333\ldots \times 2^{-2} = 0.333\,333\,333\,333\,333\,31\ldots\). \(\frac13\)에 가장 가까운 배정도 수이고, 정확한 \(\frac13\)은 아니다.
활용
- 같은지 비교하지 않는다. 파이썬에서
0.1 + 0.2 == 0.3은 거짓이다. 0.1은 2진수로 끝없이 이어지는 소수라 가장 가까운 칸으로 옮겨 적히고, 그 작은 차이가 덧셈에서 드러난다. 두 실수는 차이가 충분히 작은지로 비교한다1. - 큰 수에 작은 수를 더하면 사라진다. 단정도에서 \(2^{24} + 1\)은 \(2^{24}\)다. 간격이 2라서 1을 적을 칸이 없다. 그래서 많은 수를 더할 때는 작은 것부터 더한다1.
- 돈처럼 한 푼도 틀리면 안 되는 값은 부동소수점 대신 정수(원 단위)로 적는다1.
- 계산 도중 오차가 얼마나 불어나는지는 노름과 조건수에서 다룬다.
연결
- 선수: 정수의 2진 표현과 2의 보수, 진법과 자릿수
- 음수 지수: 거듭제곱과 지수법칙
- 같은 비트를 정수로 읽을지 실수로 읽을지: 데이터와 메타데이터
자주 하는 오해
틀렸다. 화면에 0.1이라고 찍히니 0.1이 그대로 들어 있다고 믿기 쉽다. 하지만 64비트로 만들 수 있는 줄은 \(2^{64}\)가지뿐이고 실수는 끝없이 많다. 0.1조차 2진수로는 0.000110011…으로 끝없이 이어져 가장 가까운 칸에 적힌다. 확인하는 법: 0.1 + 0.2 == 0.3을 실행하면 거짓이 나온다. 반대로 \(0.5\), \(0.15625\)처럼 분모가 2의 거듭제곱인 수와 \(2^{53}\) 이하의 정수는 정확히 적힌다1.
확인 문제
C1 \(-6.5\)를 IEEE 754 단정도 32비트로 적어라.
답: \(6.5 = 110.1_2 = 1.101_2 \times 2^2\). 부호 1. 지수 \(2 + 127 = 129 =\) 10000001. 가수는 맨 앞 1을 떼고 101 뒤에 0을 20개. 1 10000001 10100000000000000000000.
흔한 오답: 지수에 127을 더하지 않고 00000010을 쓰는 것, 가수에 맨 앞 1까지 적어 1101…로 쓰는 것.
C2 부동소수점에서 이웃한 두 수의 간격이 수가 클수록 벌어지는 이유는?
답: 유효 숫자의 칸 수(단정도 24비트)가 정해져 있기 때문이다. 가수의 마지막 칸의 무게는 \(2^{-23} \times 2^{\text{지수}}\)라서, 지수가 1 커지면 간격이 두 배가 된다. 1 근처에서는 \(2^{-23}\)이던 간격이 \(2^{24}\) 근처에서는 2다.
C3 다음 중 배정도로 정확히 적을 수 있는 수를 고르라. (가) 0.1 (나) 0.15625 (다) \(2^{53} + 1\) (라) 1,000,000
답: (나)와 (라). (나)는 \(\frac{5}{32}\)로 분모가 2의 거듭제곱이다. (라)는 \(2^{53}\)보다 작은 정수다. (가)는 2진수로 끝없이 이어지는 소수라 못 적는다. (다)는 \(2^{53}\) 위에서 간격이 2라 홀수를 못 적고 \(2^{53}\)이 된다.