N Log

부동 소수점 (Floating Point Number)

들어가는 말

우리는 진법 변환에서 10진수 소수를 2진수 소수로 변환하는 방법을 배웠다.
이렇게 변환한 수를 고정 소수점 방식으로 저장하면 소수점의 위치가 정해져 있기 때문에, 한정된 비트 안에서 표현할 수 있는 수의 범위가 제한된다.

한정된 비트로 더 넓은 범위의 수를 표현하려면, 가수(significand)는 유지하면서 수의 크기를 조절할 방법이 필요하다.
지수를 이용하면 같은 가수로도 수의 크기를 아주 크게 늘리거나 아주 작게 줄일 수 있다.
예를 들어 1.2345×10111.2345 \times 10^{11}123,450,000,000123{,}450{,}000{,}000이고, 1.2345×10111.2345 \times 10^{-11}0.0000000000123450.000000000012345이다.
두 식은 모두 1.23451.2345라는 같은 가수를 사용하지만, 지수에 따라 소수점이 놓이는 위치와 값의 크기가 달라진다.

이처럼 가수와 지수를 이용해 소수점의 위치를 고정하지 않고 수를 표현하는 방식을 부동 소수점이라고 한다.
여기서 ‘부동’은 움직이지 않는다는 뜻의 부동(不動)이 아니라, 물이나 공기처럼 떠서 움직인다는 뜻의 부동(浮動)이다.
부동소수점을 알아보기 전 필요한 개념들을 먼저 살펴보자.

글을 읽어나가다 중간 계산을 직접 확인하고 싶다면, 다음 도구를 활용해보자.

가수(Significand)

가수(significand)는 수를 m×bnm \times b^n 형태로 표현했을 때 mm에 해당하는 부분을 말한다.
예를 들어 1.230×1031.230 \times 10^3에서 가수는 1.2301.230이다.

같은 수라도 표현 방법에 따라 가수의 표기는 달라질 수 있다.
예를 들어 0.004500.00450을 단순히 수치적인 값으로만 보면 0.00450.0045와 같으므로 0.0045=4.5×1030.0045 = 4.5 \times 10^{-3}으로 표현할 수 있고, 이때 가수는 4.54.5이다.
반면 원래 표기의 마지막 00까지 보존하면 0.00450=4.50×1030.00450 = 4.50 \times 10^{-3}으로 표현할 수 있으며, 이 경우 가수는 4.504.50이다.
컴퓨터에서 일반적으로 사용하는 IEEE 754 이진 부동소수점 방식에서는 0.00450.00450.004500.00450이 같은 비트 패턴으로 저장되므로 마지막 00의 정밀도 정보는 보존되지 않는다.

가수(significand)를 mantissa라고 부르는 경우도 많다.
다만 mantissa는 원래 로그에서 정수 부분과 구별되는 소수 부분을 가리키는 용어이므로, significand를 mantissa라고 부르는 것은 관습적인 표현에 가깝다.

과학적 표기법(Scientific Notation)

과학적 표기법은 수를 표기하는 방법 중의 하나이며, 아주 큰 수나 작은 수를 편하게 작성 할 수 있다.

10진수의 과학적 표기법: m×10nm \times 10^n
12345678900010=123456789×103123456789000_{10} = 123456789 \times 10^{3}

2진수의 과학적 표기법: m×2nm \times 2^n
101.1010102=1011010102×26101.101010_2 = 101101010_2 \times 2^{-6}

  • mm은 가수(Significand, Fraction, Mantissa)이며 실수로 이루어져 있다.
  • nn은 지수(Exponent)이고 정수로 이루어져 있고 소수점의 위치를 나타낸다.

정규화한 과학적 표기법(Normalized Scientific Notation)

정규화(normalization)는 여러 방식으로 표현할 수 있는 값을 일정한 규칙에 따라 표준 형태로 바꾸는 과정이다.
정규화하지 않으면 소수점의 위치를 다르게 정할 수 있어, 같은 수를 여러 과학적 표기법으로 나타낼 수 있다.

12345=1.2345×10412345 = 1.2345 \times 10^4 12345=12.345×10312345 = 12.345 \times 10^3 12345=123.45×10212345 = 123.45 \times 10^2

10진수의 정규화한 과학적 표기법: m×10nm \times 10^n (1m<101 \leq m < 10)
1234510=1.2345×10412345_{10} = 1.2345 \times 10^{4}

2진수의 정규화한 과학적 표기법: m×2nm \times 2^n (12m<1021_2 \leq m < 10_2)
101.1010102=1.0110102×22101.101010_2 = 1.011010_2 \times 2^{2}

IEEE 754 binary32

IEEE 754는 컴퓨터가 부동 소수점 수를 저장하고 연산하는 방식을 정의한 표준이다.
IEEE 754에서 널리 쓰이는 형식에는 binary32(32비트 단정밀도, Single Precision)와 binary64(64비트 배정밀도, Double Precision)가 있다.
이 글에서는 binary32에 집중해 살펴본다.
다음 그림은 실수를 binary32 형식으로 저장할 때 각 비트 필드가 어떤 역할을 맡는지 보여준다.

 31  30                       23 22                                                               0
+---+---------------------------+-----------------------------------------------------------------+
| S |  Exponent field (8 bits)  |                       Fraction field (23 bits)                  |
+---+---------------------------+-----------------------------------------------------------------+

S = Sign (1 bit)

실수는 그림처럼 세 필드로 나누어 저장하지만, 2진수 표현을 그대로 나누어 저장하지는 않는다.
먼저 2진수를 1.xxxx×2n1.xxxx \times 2^n 꼴의 정규화한 과학적 표기법으로 변환한다.
이때 지수와 가수에는 값을 저장하고 복원할 때 별도의 규칙이 있다.

필드 비트 수 역할
부호 비트(Sign) 1 양수면 0을, 음수면 1을 저장한다.
지수(Exponent) 8 수의 크기를 저장한다.
가수(Fraction, Significand, Mantissa) 23 가수의 소수 부분을 저장한다.

지수 바이어스(Bias)

지수 필드는 실제 지수에 바이어스(bias) 127을 더한 값을 저장한다.
값을 복원할 때는 저장된 값에서 바이어스 127을 뺀다.
예를 들어 1.xxxx×231.xxxx \times 2^{3}은 3 + 127 = 131을 저장하므로 지수 필드는 1000 0011이 된다.

값을 저장할 때 127을 더하고 복원할 때 127을 빼므로 실제 지수에는 영향을 주지 않는다.
그렇다면 바이어스는 왜 사용할까?

바이어스는 지수의 크기 비교를 단순하게 만들기 위해 사용한다.
부동 소수점 표현은 정규화한 과학적 표기법을 기반으로 표현하므로, 지수만 비교해도 수의 크기를 비교할 수 있다.
하지만 실제 지수를 2의 보수로 저장하면 비트 패턴의 순서가 지수의 크기 순서와 일치하지 않는다.
따라서 지수 필드를 부호 없는 정수처럼 단순 비교하기 어렵다.
이를 직관적으로 보기 위해 먼저 2의 보수 표현을 비트 패턴으로 살펴보자.

10진수 2진수 2의 보수
-128 1000 0000
-127 1000 0001
-1 1111 1111
0 0000 0000
1 0000 0001
127 0111 1111

10진수 값이 1씩 증가하면 2의 보수 비트 패턴도 순서대로 증가한다.
하지만 음수와 양수가 맞닿는 지점에서는 비트 패턴이 1111 1111에서 0000 0000으로 바뀐다.
따라서 비트 패턴을 부호 없는 정수로 해석하면 -10보다 크게 되어, 한 번의 단순 비교만으로 지수의 크기를 판단할 수 없다.
2의 보수 방식을 사용한다면 부호 비트를 확인하거나 비트 패턴을 부호 있는 정수로 해석하는 처리가 필요하다.

반면 바이어스를 적용한 비트 패턴을 살펴보자.

bias를 더한 값 비트 패턴
-126 1 0000 0001
-125 2 0000 0010
-1 126 0111 1110
0 127 0111 1111
1 128 1000 0000
126 253 1111 1101
127 254 1111 1110

표에 -127128이 없는 이유는 해당 값이 특수값을 나타내는 지수 필드와 연결되기 때문이다.
-127에 대해서는 정규화 수에서 128은 특수값들에서 살펴본다.

실제 지수에 127을 더하면 음수 지수까지 0 이상의 값으로 저장할 수 있다.
덕분에 지수 필드를 부호 없는 정수처럼 비교해 실제 지수의 크기를 쉽게 판단할 수 있다.

가수(Significand)

가수를 저장할 때 흥미로운 점은 Fraction field에는 가수의 소수 부분만 저장한다는 것이다.
2진수의 정규화한 과학적 표기법: m×2nm \times 2^n (12m<1021_2 \leq m < 10_2)
이 표기법을 보면 가수의 정수부는 항상 11이라는 것을 알 수 있다.

따라서 맨 앞의 1은 항상 존재하므로 실제로 저장하지 않고, 소수점 뒤의 비트만 Fraction field에 저장한다.
이 생략된 1을 hidden bit(또는 implicit leading 1)라고 한다.

예시로 0.31250.3125를 2진수로 변환한 뒤 정규화해보자.

0.3125×2=0.62500.625×2=1.2510.25×2=0.500.5×2=1.010.312510=0.01012=1.012×22\begin{aligned} 0.3125 \times 2 &= 0.625 &&\Rightarrow 0 \\ 0.625 \times 2 &= 1.25 &&\Rightarrow 1 \\ 0.25 \times 2 &= 0.5 &&\Rightarrow 0 \\ 0.5 \times 2 &= 1.0 &&\Rightarrow 1 \\ \therefore\quad 0.3125_{10} &= 0.0101_2 = 1.01_2 \times 2^{-2} \end{aligned}

이 예시에서는 가수 1.0121.01_2에서 hidden bit를 제외한 01201_2을 Fraction field에 저장한다.
Fraction field는 23비트이므로, 나머지 비트는 오른쪽을 00으로 채워 01000000000000000000000201000000000000000000000_2으로 저장한다.
값을 복원할 때는 Fraction field의 앞에 11을 추가해 가수를 만든다.
따라서 실제로 저장하는 비트는 23개지만, 가수는 11과 소수부를 합쳐 24비트로 표현된다.

정규화 수(Normalized Numbers)

IEEE 754에서 지수 필드가 모두 0도 아니고 모두 1도 아닌 값은 정규화 수이다.
정규화 수는 앞에서 살펴본 정규화한 이진 과학적 표기법처럼 가수부가 항상 1.xxxx... 형태를 가진다.
32비트 단정밀도 부동소수점에서 정규화 수의 값은 다음과 같이 계산한다.

(1)s×2e127×(1.b1b2b23)2(-1)^{\mathrm{s}} \times 2^{e - 127} \times \left(1.b_1b_2\cdots b_{23}\right)_2

다음 32비트 패턴을 정규화 수 공식에 대입해 값을 계산해보자.

부호(1비트) 지수 필드(8비트) 가수 필드(23비트)
0 1000 0001 010 1000 0000 0000 0000 0000

부호 비트가 0이므로 부호 항은 (1)0=1(-1)^0 = 1이다.
지수 필드 1000 0001129129이므로 실제 지수는 129127=2129 - 127 = 2이다.
가수 필드의 앞에 hidden bit 1을 붙이면 1.01012=1.25101.0101_2 = 1.25_{10}가 된다.
따라서 값은 1×22×1.25=5.251 \times 2^2 \times 1.25 = 5.25이다.

비정규화 수(Denormalized Numbers)

정규화 수는 가수부가 항상 1.xxxx... 형태를 가지므로, 표현할 수 있는 수에 한계가 있다.

1.02×21261.18×10381.0_2 \times 2^{-126} \approx 1.18 \times 10^{-38}

특히 정규화 수 공식만으로는 최종 결과를 0으로 만들 수 없다.
ss가 0이면 부호 항은 11이고, ss가 1이면 부호 항은 1-1이다.
지수 항에서 ee가 어떤 값이더라도, 2의 거듭제곱은 0이 될 수 없다.
가수 항은 정수 부분이 항상 11이므로 0이 될 수 없다.
따라서 세 항을 곱한 정규화 수의 값은 0이 될 수 없다.

IEEE 754는 0과 가장 작은 양의 정규화 수 사이의 간격을 메우기 위해 비정규화 수를 사용한다.
비정규화 수는 Denormalized Number 또는 Subnormal Number라고도 한다.
지수 필드가 0000 0000이고 가수 필드가 0이 아닌 경우가 비정규화 수에 해당한다.

(1)s×2126×(0.b1b2b23)2(-1)^{\mathrm{s}} \times 2^{-126} \times \left(0.b_1b_2\cdots b_{23}\right)_2

정규화한 과학적 표기법에서 지수가 126-126 미만인 값은 지수 필드를 0000 0000으로 저장하고, 비정규화 수 공식으로 해석한다.
공식에서 지수는 126-126으로 고정한다.
작은 값을 표현하기 위해 지수를 낮추는 대신, 0에 가까운 가수를 곱해 작은 값을 표현한다.

비정규화 수에서 가장 작은 양의 값은 가수부의 마지막 비트(b23b_{23})만 11인 경우가 가장 작은 양의 비정규화 수이다.

21491.4×10452^{-149} \approx 1.4 \times 10^{-45}

특수값들

지수 필드 가수 필드 의미
0000 0000 0 +0 또는 -0
0000 0000 0이 아님 비정규화 수
0000 0001 ~ 1111 1110 모든 값 정규화 수, 실제 지수 -126 ~ 127
1111 1111 0 +∞ 또는 -∞
1111 1111 0이 아님 NaN

8.75를 32비트로 저장하기

1. 10진수를 2진수로 변환하기

먼저 정수부와 소수부를 나눠서 2진수로 표현한다.

구분 10진수 2진수
정수부 8108_{10} 100021000_2
소수부 0.75100.75_{10} 0.1120.11_2

8.7510=1000.1128.75_{10} = 1000.11_2

2. 2진수를 정규화하기

1000.1121000.11_2를 정규화한 과학적 표기법으로 표현한다.

1000.112=1.000112×231000.11_2 = 1.00011_2 \times 2^3

3. 부호, 지수 필드, 가수 필드 채우기

부호 (1비트) 지수 필드 (8비트) 가수 필드 (23비트)
0 10000010 00011000000000000000000

양수이니 부호 비트에는 0을 저장한다.
지수 필드에는 지수 3과 바이어스 127을 더해 130을 저장한다.
가수 필드에는 가수의 정수를 제외한 00011200011_2를 저장한다.

4. 최종 비트 패턴

8.75의 IEEE 754 단정밀도 비트 패턴은 01000001000011000000000000000000가 된다.

이를 4비트씩 묶으면 0100 0001 0000 1100 0000 0000 0000 0000이므로, 16진수로는 0x410C0000이다.

0x415A0000를 10진수로 해석하기

0x415A0000는 32비트이고 4비트씩 묶으면 0100 0001 0101 1010 0000 0000 0000 0000이다.

부호 (1비트) 지수 필드 (8비트) 가수 필드 (23비트)
0 10000010 10110100000000000000000

지수 필드는 10진수로 130이며 바이어스 127을 빼면 실제 지수는 3이 된다.

가수 필드를 복원할 때는 저장할 때 생략한 정수부 11을 다시 더한다.
그 뒤 각 비트에 해당 자릿값을 곱해 모두 더하면 소수부를 구할 수 있다.

(1+1×21+1×23+1×24+1×26)×23=(1+0.5+0.125+0.0625+0.015625)×23=1.703125×23=1.703125×8=13.625\begin{aligned} & (1 + 1 \times 2^{-1} + 1 \times 2^{-3} + 1 \times 2^{-4} + 1 \times 2^{-6}) \times 2^3 \\ &= (1 + 0.5 + 0.125 + 0.0625 + 0.015625) \times 2^3 \\ &= 1.703125 \times 2^3 \\ &= 1.703125 \times 8 \\ &= 13.625 \end{aligned}

0.1을 32비트로 저장하고 복원하기

1. 10진수를 2진수로 변환하기

0.1×2=0.200.2×2=0.400.4×2=0.800.8×2=1.610.6×2=1.210.2×2=0.400.4×2=0.800.8×2=1.610.6×2=1.210.2×2=0.400.4×2=0.800.8×2=1.61\begin{aligned} 0.1 \times 2 &= 0.2 &&\Rightarrow 0 \\ 0.2 \times 2 &= 0.4 &&\Rightarrow 0 \\ 0.4 \times 2 &= 0.8 &&\Rightarrow 0 \\ 0.8 \times 2 &= 1.6 &&\Rightarrow 1 \\ 0.6 \times 2 &= 1.2 &&\Rightarrow 1 \\ 0.2 \times 2 &= 0.4 &&\Rightarrow 0 \\ 0.4 \times 2 &= 0.8 &&\Rightarrow 0 \\ 0.8 \times 2 &= 1.6 &&\Rightarrow 1 \\ 0.6 \times 2 &= 1.2 &&\Rightarrow 1 \\ 0.2 \times 2 &= 0.4 &&\Rightarrow 0 \\ 0.4 \times 2 &= 0.8 &&\Rightarrow 0 \\ 0.8 \times 2 &= 1.6 &&\Rightarrow 1 \end{aligned}

0.10.1을 2진수로 변환하면 소수부가 반복된다.
따라서 0.1100.1_{10}0.0001120.0\overline{0011}_2가 된다.

2. 2진수를 정규화하기

0.0001120.0\overline{0011}_2 또는 0.000110011001120.0001100110011\ldots_2를 정규화한 과학적 표기법으로 표현하면 다음과 같다.

0.00011001100112=1.10011001100112×240.0001100110011\ldots_2 = 1.1001100110011\ldots_2 \times 2^{-4} 0.000112=1.10012×240.0\overline{0011}_2 = 1.\overline{1001}_2 \times 2^{-4}

3. 부호, 지수 필드, 가수 필드 채우기

부호 (1비트) 지수 필드 (8비트) 가수 필드 (23비트)
0 01111011 10011001100110011001101

양수이므로 부호 비트에는 0을 저장한다.
지수 필드에는 지수 -4와 바이어스 127을 더한 123을 저장한다.

가수의 소수부는 무한히 반복되지만, 가수 필드에는 23비트만 저장할 수 있다.
따라서 저장할 수 있는 마지막 가수 비트인 2232^{-23} 자리 다음의 2242^{-24} 자리 비트를 확인해 반올림한다.

100 1100 1100 1100 1100 1100 1100 1100
                             ^

^2242^{-24} 자리 비트를 가리킨다.

반올림 전: 100 1100 1100 1100 1100 1100
반올림 후: 100 1100 1100 1100 1100 1101

4. 최종 비트 패턴

0.10.1의 IEEE 754 단정밀도 비트 패턴은 00111101110011001100110011001101가 된다.

이를 4비트씩 묶으면 0011 1101 1100 1100 1100 1100 1100 1101이므로, 16진수로는 0x3DCCCCCD이다.

5. 10진수로 복원하기

유한한 비트만으로는 0.10.1을 2진수로 정확히 표현할 수 없으므로, 앞서 저장한 비트 패턴은 0.10.1의 근삿값이다.
이 비트 패턴을 다시 10진수로 변환해 보면, 정확히 0.10.1이 아닌 다른 값이 나타난다.

부호 (1비트) 지수 필드 (8비트) 가수 필드 (23비트)
0 01111011 10011001100110011001101

지수 필드는 10진수로 123이고 바이어스 127을 빼면 실제 지수는 -4가 된다.

가수 필드에 저장할 때는 반올림을 했지만 복원할 때는 비트를 그대로 해석한다.

(1+21+24+25+28+29+212+213+216+217+220+221+223)×24\begin{aligned} \bigl(&1 + 2^{-1} + 2^{-4} + 2^{-5} + 2^{-8} + 2^{-9} \\ &+ 2^{-12} + 2^{-13} + 2^{-16} + 2^{-17} \\ &+ 2^{-20} + 2^{-21} + 2^{-23}\bigr) \\ &\times 2^{-4} \end{aligned} (1+0.5+0.0625+0.03125+0.00390625+0.001953125+0.000244140625+0.0001220703125+0.0000152587890625+0.00000762939453125+0.00000095367431640625+0.000000476837158203125+0.00000011920928955078125)×0.0625\begin{aligned} \bigl(&1 + 0.5 + 0.0625 + 0.03125 + 0.00390625 + 0.001953125 \\ &+ 0.000244140625 + 0.0001220703125 + 0.0000152587890625 \\ &+ 0.00000762939453125 + 0.00000095367431640625 \\ &+ 0.000000476837158203125 + 0.00000011920928955078125\bigr) \\ &\times 0.0625 \end{aligned} 1.60000002384185791015625×0.06251.60000002384185791015625 \times 0.0625 0.12=0.10000000149011611938476562510\therefore 0.1_2 = 0.100000001490116119384765625_{10}

이처럼 실수를 유한한 비트로 근사해 표현하므로, 실제 값과 저장하거나 계산한 값 사이에는 오차가 생길 수 있다.
이를 부동 소수점 오차라고 한다.
그렇다면 문제는 32비트 단정밀도(Single Precision) 자체에 있을까?
그렇지 않다.
유한한 비트만으로 모든 실수를 정확하게 나타낼 수 없는 이상, 부동 소수점 오차는 피할 수 없다.
이제 어느 범위까지 신뢰하고 사용할 수 있는지 살펴보자.

float 유효숫자 6·9·약 7자리

실수를 유한한 비트로 저장하므로, 모든 10진수를 정확하게 표현할 수 없다.
그래서 float를 얼마나 믿고 사용할 수 있는지 찾아보면 다음과 같은 설명을 자주 만나게 된다.
“유효숫자 또는 정밀도는 6~9자리다.”, “유효숫자 또는 정밀도는 약 7자리(7.22자리 또는 7.225자리)”

처음에는 표현이 제각각인 것처럼 보이지만, 이 숫자들은 같은 기준으로 얻은 값이 아니다.
6자리와 9자리라는 설명이 성립하는 각각의 상황이 있다.
서로 다른 상황을 하나의 문장으로 표현하면서 그렇게 된 것으로 보인다.
각 숫자가 어떤 기준에서 나오는지 차례로 살펴보자.

유효숫자 6자리: 10진수 왕복 변환

유효숫자 6은 decimal -> float -> decimal 맥락에서 나오는 말이다.

If a decimal string with at most 6 sig. dec. is converted to Single
and then converted back to the same number of sig. dec.,
then the final string should match the original.

출처: W. Kahan, Lecture Notes on the Status of IEEE Standard 754 for Binary Floating-Point Arithmetic, p. 4

해석하면 다음과 같다.

유효 숫자가 최대 6자리인 10진수 표기를 단정밀도 부동소수점 형식으로 변환한 뒤,
다시 동일한 개수의 유효 숫자를 갖는 10진수 표기로 변환하면,
최종 결과는 원래의 10진수 표기와 일치해야 한다.

이번에는 C 언어의 <float.h>에 정의된 매크로를 살펴보자.
32비트 환경에서 Microsoft Visual C++의 각 매크로 값은 다음과 같다.

FLT_RADIX       2  // exponent radix
FLT_MANT_DIG   24  // # of bits in mantissa
FLT_DIG         6  // # of decimal digits of precision

주석에서 #number를 뜻한다.
각각이 더 정확하게 무엇인지는 C99 표준 초안 N1256에서 확인할 수 있다.

FLT_DIG 원문을 살펴보자.

number of decimal digits, q, such that any floating-point number with q decimal digits can be rounded into a floating-point number with p radix b digits and back again without change to the q decimal digits,

{plog10bif b is a power of 10,(p1)log10botherwise\begin{cases} p \log_{10} b & \text{if } b \text{ is a power of } 10, \\ \left\lfloor (p - 1) \log_{10} b \right\rfloor & \text{otherwise} \end{cases}

여기서 bFLT_RADIX이며, IEEE 754 단정밀도에서는 2다.
pFLT_MANT_DIG이며, IEEE 754 단정밀도에서는 24다.

(p1)log10b=(241)log102=6.923689900=6\begin{aligned} \left\lfloor (p - 1) \log_{10} b \right\rfloor &= \left\lfloor (24 - 1) \log_{10} 2 \right\rfloor \\ &= \left\lfloor 6.923689900\ldots \right\rfloor \\ &= 6 \end{aligned}

수들을 대입해서 원문을 해석하면 다음과 같다.

6자리 십진 부동소수점 수를 기수 2의 24자리 부동소수점 수로 반올림했다가, 다시 반올림하여 6자리 십진 부동소수점 수로 되돌렸을 때,
원래의 십진 숫자가 변하지 않도록 하는 십진 자릿수는 6이다.

이 내용들을 바탕으로 실제 숫자들로 어떻게 변환이 되는지 살펴보자.

유효숫자 6자리 입력의 왕복 변환

입력 10진수 정규화 저장 비트 패턴 저장 비트 패턴의 10진수 해석 정규화 6자리 되도록 반올림
123.987 1.239871.23987
×102\times 10^{2}
0x42F7F958 123.98699951171875 1.23986999511718751.2398699951171875
×102\times 10^{2}
1.239871.23987
×102\times 10^{2}
123.988 1.239881.23988
×102\times 10^{2}
0x42F7F9DB 123.98799896240234375 1.23987998962402343751.2398799896240234375
×102\times 10^{2}
1.239881.23988
×102\times 10^{2}
1.23456 1.234561.23456
×100\times 10^{0}
0x3F9E0610 1.2345600128173828125 1.23456001281738281251.2345600128173828125
×100\times 10^{0}
1.234561.23456
×100\times 10^{0}
0.123456 1.234561.23456
×101\times 10^{-1}
0x3DFCD680 0.12345600128173828125 1.23456001281738281251.2345600128173828125
×101\times 10^{-1}
1.234561.23456
×101\times 10^{-1}
987654 9.876549.87654
×105\times 10^{5}
0x49712060 987654 9.876549.87654
×105\times 10^{5}
9.876549.87654
×105\times 10^{5}
0.000000
390625
3.906253.90625
×107\times 10^{-7}
0x34D1B717 0.000000390624990131982
485763728618621826171875
3.90624990131982485763.9062499013198248576
37286186218261718753728618621826171875
×107\times 10^{-7}
3.906253.90625
×107\times 10^{-7}

이 표는 각 행을 왼쪽에서 오른쪽으로 읽으면 된다.

유효숫자가 6개인 123.987를 입력하면 컴퓨터에 단정밀도 형식으로 저장이 된다.
비트 패턴을 2진수로 표현하면 너무 길기 때문에 16진수 0x42F7F958로 적었다.
0x42F7F958를 정규화 수의 공식으로 해석하면 네 번째 열의 값이 나온다.
마지막 열에서는 유효숫자 6자리가 되도록 일곱 번째 자리에서 반올림하면 입력 10진수를 복원할 수 있다.

0.000000390625는 소수점 아래에 12자리가 있는데도 왜 복원되는지 의문이 들 수 있다.
우리가 다루는 것은 그냥 숫자가 아니라 유효숫자다.
0.000000390625에서 소수점 아래의 앞 6개의 0은 빈 자리를 채워 자릿수를 맞추기 위한 숫자일 뿐이다.
이 수를 정규화한 과학적 표기법으로 나타내면 3.90625×1073.90625 \times 10^{-7}이다.
따라서 유효숫자가 6개임을 확인할 수 있다.

유효숫자 7자리 입력: 7자리 복원 시도

입력 10진수 정규화 저장 비트 패턴 저장 비트 패턴의 10진수 해석 정규화 7자리
되도록 반올림
123.9871 1.2398711.239871
×102\times 10^{2}
0x42F7F965 123.98709869384765625 1.23987098693847656251.2398709869384765625
×102\times 10^{2}
1.2398711.239871
×102\times 10^{2}
123.9876 1.2398761.239876
×102\times 10^{2}
0x42F7F9A7 123.98760223388671875 1.23987602233886718751.2398760223388671875
×102\times 10^{2}
1.2398761.239876
×102\times 10^{2}
1.234567 1.2345671.234567
×100\times 10^{0}
0x3F9E064B 1.23456704616546630859375 1.234567046165466308593751.23456704616546630859375
×100\times 10^{0}
1.2345671.234567
×100\times 10^{0}
8600213000 8.6002138.600213
×109\times 10^{9}
0x50002736 8600213504 8.6002135048.600213504
×109\times 10^{9}
8.6002148.600214
×109\times 10^{9}
0.0009765629 9.7656299.765629
×104\times 10^{-4}
0x3A800003 0.00097656284924
5965480804443359375
9.76562849245965489.7656284924596548
08044433593750804443359375
×104\times 10^{-4}
9.7656289.765628
×104\times 10^{-4}
0.000000000
9314079
9.3140799.314079
×1010\times 10^{-10}
0x30800301 0.0000000009314079
507660721901629585
7727527618408203125
9.314079507660721901629.31407950766072190162
9585772752761840820312595857727527618408203125
×1010\times 10^{-10}
9.3140809.314080
×1010\times 10^{-10}

두 번째 열까지 보면 모든 입력값의 유효숫자가 7자리임을 알 수 있다.
입력값을 단정밀도로 저장한 결과가 세 번째 열의 비트 패턴이다.
이 비트 패턴을 10진수로 해석한 값이 네 번째 열이다.
네 번째 열을 보기 좋게 정규화한 결과가 다섯 번째 열이다.
마지막 열에서는 여덟째 자리에서 반올림해 7자리로 만들고, 입력값의 유효숫자 7자리와 일치하는지 확인한다.

따라서 유효숫자 7자리 입력값은 단정밀도로 저장한 뒤 7자리로 다시 복원해도, 원래 입력값의 유효숫자 7자리와 일치가 보장되지 않는다.

유효숫자 7자리 입력: 6자리 복원 시도

FLT_DIG가 6이라는 말을 보고 다음과 같은 궁금증을 가진 분도 있을 수 있다.
유효숫자 7자리 입력값을 단정밀도로 저장한 뒤 복원한 값을 유효숫자 6자리로 반올림하면, 입력값의 앞 유효숫자 6자리와 일치하지 않을까?
하지만 다음 표가 보여 주듯, 그렇지 않다.

입력 10진수 정규화 저장 비트 패턴 저장 비트 패턴의 10진수 해석 정규화 6자리 되도록 반올림
123.9871 1.2398711.239871
×102\times 10^{2}
0x42F7F965 123.98709869384765625 1.23987098693847656251.2398709869384765625
×102\times 10^{2}
1.239871.23987
×102\times 10^{2}
123.9876 1.2398761.239876
×102\times 10^{2}
0x42F7F9A7 123.98760223388671875 1.23987602233886718751.2398760223388671875
×102\times 10^{2}
1.239881.23988
×102\times 10^{2}
1.234567 1.2345671.234567
×100\times 10^{0}
0x3F9E064B 1.23456704616546630859375 1.234567046165466308593751.23456704616546630859375
×100\times 10^{0}
1.234571.23457
×100\times 10^{0}
123.4575 1.2345751.234575
×102\times 10^{2}
0x42F6EA3D 123.45749664306640625 1.23457496643066406251.2345749664306640625
×102\times 10^{2}
1.234571.23457
×102\times 10^{2}
123.9875 1.2398751.239875
×102\times 10^{2}
0x42F7F99A 123.9875030517578125 1.2398750305175781251.239875030517578125
×102\times 10^{2}
1.239881.23988
×102\times 10^{2}
1.000400 1.0004001.000400
×100\times 10^{0}
0x3F800D1B 1.00039994716644287109375 1.000399947166442871093751.00039994716644287109375
×100\times 10^{0}
1.000401.00040
×100\times 10^{0}

두 번째 열까지 보면 모든 입력값의 유효숫자가 7자리임을 알 수 있다.
입력값을 단정밀도로 저장한 결과가 세 번째 열의 비트 패턴이다.
이 비트 패턴을 10진수로 해석한 값이 네 번째 열이다.
네 번째 열을 보기 좋게 정규화한 결과가 다섯 번째 열이다.
마지막 열에서는 일곱째 자리에서 반올림해 6자리로 만들고, 입력값의 앞 유효숫자 6자리와 일치하는지 확인한다.

9자리: float 왕복 변환

유효숫자 9은 float -> decimal -> float 맥락에서 나오는 말이다.

If a Single Precision floating-point number is converted to a decimal string
with at least 9 sig. dec. and then converted back to Single,
then the final number must match the original.

출처: W. Kahan, Lecture Notes on the Status of IEEE Standard 754 for Binary Floating-Point Arithmetic, p. 4

해석하면 다음과 같다.

단정밀도 부동소수점 수를 유효 숫자가 최소 9자리인 10진수 표기로 변환한 뒤,
다시 단정밀도 형식으로 변환하면,
최종 결과는 원래의 부동소수점 수와 일치해야 한다.

이번에는 IEEE Std 754-2019를 살펴보자.

for binary32, Pmin(binary32) = 9

Conversions from a supported binary format bf to an external character sequence and back again
results in a copy of the original number
so long as there are at least Pmin(bf) significant digits specified
and the rounding-direction attributes in effect during the two conversions are round to nearest rounding-direction attributes.

여기서 bf는 binary format을 뜻하며, Pmin(binary32)은 9로 정의된다.
이를 대입해 해석하면 다음과 같다.

지원되는 이진 형식 bf를 외부 문자 시퀀스로 변환한 뒤 다시 되돌리는 변환은
원래 수와 동일한 수를 결과로 얻는다
단 9개 이상의 유효숫자가 지정되어 있고
두 변환에 적용되는 반올림 방향 속성이 round to nearest 반올림 방향 속성인 경우에 한한다.

이 왕복 변환 보장은 C11 표준 초안 N1570FLT_DECIMAL_DIG 항목에서도 확인할 수 있다.

이제 실제 숫자들로 살펴보자.

저장 비트 패턴 비트 패턴의 10진수 해석 7자리로 변환 시도 8자리로 변환 시도 9자리로 변환 시도
0x40307454 2.75710010528564453125 2.757100
0x40307454
2.7571001
0x40307454
2.75710011
0x40307454
0x3F9E0011 1.23437702655792236328125 1.234377
0x3F9E0011
1.2343770
0x3F9E0011
1.23437703
0x3F9E0011
0xC3BA5641 -372.673858642578125 3.726739×102-3.726739 \times 10^{2}
0xC3BA5642
3.7267386×102-3.7267386 \times 10^{2}
0xC3BA5641
3.72673859×102-3.72673859 \times 10^{2}
0xC3BA5641
0x44161DB8 600.46435546875 6.004644×1026.004644 \times 10^{2}
0x44161DB9
6.0046436×1026.0046436 \times 10^{2}
0x44161DB8
6.00464355×1026.00464355 \times 10^{2}
0x44161DB8
0x3DE6556D 0.112467624247074127197265625 1.124676×1011.124676 \times 10^{-1}
0x3DE6556A
1.1246762×1011.1246762 \times 10^{-1}
0x3DE6556C
1.12467624×1011.12467624 \times 10^{-1}
0x3DE6556D
0x3DDE3FF5 0.108520425856113433837890625 1.085204×1011.085204 \times 10^{-1}
0x3DDE3FF2
1.0852043×1011.0852043 \times 10^{-1}
0x3DDE3FF6
1.08520426×1011.08520426 \times 10^{-1}
0x3DDE3FF5

표의 각 행은 왼쪽에서 오른쪽으로 읽는다.

먼저 컴퓨터에 저장된 비트 패턴을 10진수로 해석한다.
그 값을 7개의 유효숫자로 반올림해 표기한 뒤 다시 단정밀도 형식으로 변환하면, 원래 비트 패턴으로 돌아오는 경우도 있고 다른 비트 패턴이 되는 경우도 있다.

7자리에서 원래 비트 패턴으로 돌아오지 않으면 8자리로 다시 시도한다.
8자리에서도 원래 비트 패턴으로 돌아오는 경우와 그렇지 않은 경우가 있다.

8자리에서 실패한 경우에는 9개의 유효숫자로 표기한 뒤 다시 변환한다.
표의 모든 경우가 이 단계에서는 원래 비트 패턴으로 돌아온다.

이것이 앞에서 살펴본 Pmin(binary32) = 9의 의미다.
7자리 또는 8자리만으로도 원래 비트 패턴으로 변환되는 경우가 있지만, 항상 그런 것은 아니다.
반면 9개의 유효숫자로 표기한 10진수를 다시 단정밀도 형식으로 변환하면 언제나 원래 비트 패턴과 일치한다.
따라서 앞 9개 유효숫자가 같은 10진수 표기는 단정밀도 형식에서 언제나 같은 비트 패턴으로 변환된다.

0x3F9E0652=1.23456789=1.23456789000000=1.23456789111111=1.23456789222222=1.23456789333333=1.23456789444444=1.23456789555555=1.23456789666666=1.23456789777777=1.23456789888888=1.23456789999999\begin{aligned} \mathtt{0x3F9E0652} &= 1.23456789 = 1.23456789000000 = 1.23456789111111 \\ &= 1.23456789222222 = 1.23456789333333 = 1.23456789444444 \\ &= 1.23456789555555 = 1.23456789666666 = 1.23456789777777 \\ &= 1.23456789888888 = 1.23456789999999 \end{aligned}

약 7자리: 비트 수의 환산

32비트 단정밀도에서 fraction field는 23비트다.
정규화 수는 값을 복원할 때 선행 비트 1이 암묵적으로 붙으므로, 총 24개의 유효 비트를 사용한다.
이 24비트로는 224=16,777,2162^{24} = 16{,}777{,}216가지 서로 다른 경우를 나타낼 수 있다.
10진수에서 몇 자리가 있어야 2242^{24}개를 나타낼 수 있는지 계산해 보자.

log10(224)=24log10224(0.30103)7.2247\log_{10}(2^{24}) = 24 \log_{10} 2 \approx 24(0.30103) \approx 7.2247

7.227.225라는 표현은 여기서 비롯되었다.
이 숫자는 float이 약 7자리의 10진수 유효숫자를 표현할 수 있음을 의미한다.
다만 이는 24비트의 정보량을 10진수 자릿수로 환산한 값일 뿐, 실제로 보장되는 정밀도를 의미하지는 않는다.

IEEE 754 binary64

 63  62                       52 51                                                               0
+---+---------------------------+-----------------------------------------------------------------+
| S | Exponent field (11 bits)  |                       Fraction field (52 bits)                  |
+---+---------------------------+-----------------------------------------------------------------+

S = Sign (1 bit)

비트 수가 늘어나면서 표현할 수 있는 값의 범위와 정밀도가 높아졌다.
binary64에서는 바이어스로 127 대신 1023을 사용한다.
decimal → double → decimal 변환에서는 유효숫자 15자리까지 원래 값을 복원할 수 있다.
double → decimal → double 변환에서는 유효숫자를 17자리로 표기하면 언제나 원래 비트 패턴으로 복원할 수 있다.