Proven C BookEnglish GitHub

8 수의 표현 — IEEE 754라는 계약

먼저 알아야 할 것

7장 정수의 표현 · 고정된 비트로 수를 표현할 때 생기는 한계

돌아보기

7장에서 부호 있는 정수까지 비트에 담았다 — 음수를 담는 세 가지 방법이 겨루다 2의 보수로 정리되는 것까지 보았다. 그런데 여전히 전부 정수다. 1.5 같은 수는 — 비트로 어떻게 담는가?

답. 비트 자체에는 소수점이 없다. 그래서 소수점은 약속으로 만든다 — 음수를 “약속”(2의 보수)으로 만들었던 것과 같은 이치다. “소수점이 어디에 있다고 치고 읽을 것인가”를 정하는 방식이 두 갈래로 갈리는데, 소수점을 못박아 두는 쪽(고정)과 소수점을 데이터에 실어 움직이는 쪽(부동)이다. 이 장이 그 두 갈래의 이야기다.

이 장의 필요성과 맥락

사다리의 둘째 단이다. 이 장을 뒤로 미룰 수 없는 이유는 하나다 — 컴퓨터의 수가 근사라는 사실이 이 책에서 가장 자주 되돌아오는 사실이기 때문이다. 22장의 %f, 50장의 비트 뜯어보기, 73장의 <math.h> 가 모두 여기서 세운 IEEE 754 계약을 다시 부른다. 근사를 늦게 배우면 그때까지 쓴 코드를 다시 봐야 한다.

이 장이 끝나면

표현의 사다리 첫 단이다. 소수점 있는 수를 사물함에 담는 두 가지 방법 — 고정소수점과 부동소수점 — 을 구별하게 되고, 부동소수점의 난립을 통일한 IEEE 754라는 계약을 만난다. 컴퓨터의 수가 유한하고 근사적이라는, 이 책에서 가장 중요한 사실 하나가 여기서 자리 잡는다.

이 장에서 답할 질문

  1. 왜 지수에 바이어스를 더하는가 — 그냥 2의 보수로 담으면 안 되는가?
  2. 32비트 float와 64비트 double은 실무에서 어떻게 갈라 쓰는가?
  3. 7장의 정수 오버플로와 이 장의 반올림은 둘 다 “유한한 그릇” 문제인데, 성격이 같은 문제인가?

8.1 고정소수점 — 소수점을 약속으로 못박다

첫 번째 방법은 놀랄 만큼 단순하다. 그냥 정수를 쓰되, 단위를 바꾼다.

돈 계산이 좋은 예다. 1,500.25원을 저장하고 싶으면, “우리 장부는 전부 전(錢, 0.01원) 단위로 적는다”고 약속하고 정수 150025를 저장하면 된다. 소수점은 어디에도 저장되지 않는다 — 읽고 쓰는 모든 사람이 “끝에서 두 자리 앞에 소수점이 있다고 친다”는 약속을 공유할 뿐이다. 이것이 고정소수점(fixed point)이다. 소수점의 위치가 약속으로 고정되어 있다.

고정소수점의 미덕은 정수 그 자체라는 것이다. 더하기와 빼기가 정수 연산만큼 빠르고 정확하며, 어림이 없다. 그래서 금액 계산과, 소수점 하드웨어가 없는 작은 임베디드 칩에서 지금도 현역이다. 약점은 뻣뻣함이다 — 아주 큰 수와 아주 작은 수를 한 약속으로 감당할 수 없다. 전 단위 장부에 은하의 질량과 원자의 질량을 같이 적을 수는 없는 노릇이다.

8.2 부동소수점 — 소수점을 데이터에 싣다

두 번째 방법은 과학 시간에 배운 과학적 표기법을 기계에 옮긴 것이다. 6.02×1023처럼 수를 “알맹이 숫자”와 “10을 몇 번 곱했는가”로 나눠 적는 방식 말이다. 알맹이(가수, significand)와 곱한 횟수(지수, exponent)를 둘 다 데이터로 저장하면, 소수점이 지수를 따라 떠다닌다 — 그래서 부동소수점(floating point)이다. 기계는 10 대신 2를 쓴다: 수를 가수×2지수 꼴로 담는다.

이 방식의 힘은 같은 비트 수로 어마어마한 범위를 감당한다는 것이다. 은하도 원자도 한 형식에 담긴다. 대가는 정밀도다 — 알맹이 숫자의 자리 수가 유한하므로, 그 자리 수를 넘는 부분은 반올림으로 버려진다.

흔한 오해. “컴퓨터는 수학을 잘하니까, 소수 계산은 당연히 정확하다”

그럴듯하다 — 계산기가 틀리는 것을 본 적이 없으니까. 그러나 부동소수점이 담을 수 있는 수는 유한 개뿐이고, 담지 못하는 수는 가장 가까운 이웃으로 반올림된다. 놀랍게도 0.1조차 그렇다 — 0.1은 2진법으로 무한소수라서 (아래 수학 박스), 기계 속의 “0.1”은 사실 0.1에 아주 가까운 다른 수다. 작은 어긋남은 계산을 거치며 쌓일 수 있다. 이것은 컴퓨터의 결함이 아니라 유한한 표현의 필연이고, 잘 다루는 법이 따로 있다(50장). 지금 기억할 것은 하나다: 부동소수점은 정확한 수가 아니라 성실한 근사다.

수학. 왜 0.1은 2진법으로 못 떨어지는가

유한 소수로 떨어지는 분수는 분모가 기수의 거듭제곱 인수로만 이루어진 경우뿐이다. 십진법(기수 10=2×5)에서 110은 한 자리로 끝난다. 그러나 2진법의 기수는 2뿐이라서, 분모에 5가 들어 있는 110은 절대 유한하게 끝나지 않는다:

0.110=0.00011001100112

13이 십진법에서 0.333으로 무한한 것과 같은 이치다. 유한한 가수는 이 무한을 어딘가에서 잘라야 하고, 그 잘림이 근사의 근원이다.

8.3 비트가 실제로 어떻게 나뉘는가

“가수와 지수를 함께 담는다”를 비트 배치까지 내려가 보자. IEEE 754는 한 값을 세 조각으로 나눈다 — 부호(sign) 1비트, 지수(exponent), 가수(fraction).

형식전체부호지수가수
float(단정도)32비트1823
double(배정도)64비트11152

표 8.1

ieee754

그림 8.1 — floatdouble 의 비트 배치. 칸의 너비가 비트 수의 비율이다.

세 조각을 읽는 규칙은 이렇다.

즉 정규수의 값은 이렇게 복원된다.

(1)부호×1.가수×2저장된 지수바이어스

지수 자리의 양 끝은 특별한 뜻으로 예약돼 있다.

지수 비트가수
전부 00영(부호 비트에 따라 +0.0 또는 -0.0)
전부 00이 아님비정규수(subnormal) — 숨은 비트를 0으로 보고, 0 근처를 촘촘히 메운다
전부 10무한대(부호에 따라 ±)
전부 10이 아님NaN — “수가 아님”
그 밖무엇이든정규수 — 위의 복원식

표 8.2

이 표가 50장에서 만날 성질들의 출처다. NaN이 자기 자신과 같지 않은 것도, 무한대가 넘침의 결과로 나오는 것도, +0.0 == -0.0이면서 비트는 다른 것도 전부 이 배치에서 나온다. 50장에서 실제 비트를 찍어 하나씩 확인한다.

문. 왜 지수에 바이어스를 더하는가 — 그냥 2의 보수로 담으면 안 되는가?

답. 담을 수는 있다. 그러나 바이어스 방식에는 실용적인 이득이 있다 — 부호를 제외한 비트열을 정수처럼 비교하면 실수의 크기 순서가 그대로 나온다. 지수가 앞자리에 있고 바이어스 덕에 작은 지수가 작은 비트열이 되므로, 같은 부호의 두 양수는 비트 패턴을 부호 없는 정수로 읽어 비교해도 대소가 맞는다. 하드웨어의 비교 회로가 단순해지고, 정렬 같은 작업도 빨라진다. 2의 보수 지수였다면 이 성질이 깨진다.

8.4 근사가 일으키는 세 가지 사건

“성실한 근사”가 실전에서 어떤 얼굴로 나타나는지, 수치로 미리 구경해 둔다. (여기서는 지면 계산으로 보이고, C 코드로 직접 실행해 보이는 것은 50장이다.)

사건 1 — 0.1 + 0.2 ≠ 0.3. 프로그래밍 세계에서 가장 유명한 등식 아닌 등식이다. 방금 본 대로 0.1도 0.2도 0.3도 2진법으로는 무한소수라서, 기계에는 각각 “가장 가까운 이웃”이 대신 담긴다. 그런데 0.1의 이웃과 0.2의 이웃을 더한 결과가, 하필 0.3의 이웃과 다른 쪽으로 반올림된다. 64비트 형식으로 실제 값을 적으면 —

둘 다 0.3에 성실하게 가깝지만, 서로 같지는 않다. 그래서 “같은가?”라고 물으면(==) 기계는 정직하게 “아니오”라고 답한다.

내부 표현을 직접 들여다보면 사건의 전모가 한눈에 보인다. 64비트 형식은 [부호 1비트 | 지수 11비트 | 가수 52비트]로 나뉘는데, 이 네 수의 64비트를 16진법으로 적으면 —

64비트 내부 표현 (16진)
0.13FB9 9999 9999 999A
0.23FC9 9999 9999 999A
0.33FD3 3333 3333 3333
0.1 + 0.23FD3 3333 3333 3334

표 8.3

읽을거리가 세 겹이다. 첫째, 0.1과 0.2의 가수에 늘어선 9999...는 2진 무한소수 0011의 순환이 16진법으로 보인 모습이고, 끝자리가 9가 아니라 A인 것은 무한을 52비트에서 자르며 반올림해 올린 흔적이다 — 수학 박스에서 본 “잘림”이 비트에 그대로 찍혀 있다. 둘째, 0.3의 가수 3333...도 같은 순환의 다른 위상이며, 이쪽은 마지막에서 내림이 됐다. 셋째 — 핵심이다 — 0.3과 0.1+0.2는 마지막 비트 하나만 다르다. 눈금 하나(전문 용어로 1 ulp) 차이의 바로 옆 이웃인 것이다. ==는 이 한 비트의 차이도 다름으로 답하므로, 부동소수점의 “같음”은 눈금 하나의 어긋남에도 깨진다.

사건 2 — 그러면 비교는 어떻게 하는가. 부동소수점의 세계에서 “같다”는 질문 자체를 바꿔야 한다 — “정확히 같은가”가 아니라 “충분히 가까운가” 로. 허용 오차(전통적으로 엡실론, 𝜀이라 부른다)를 하나 정하고, 두 수의 차이가 그 안에 들면 같다고 치는 것이다:

|𝑎𝑏|<𝜀(예: 𝜀=109)

이것이 기본형이고, 실전에는 한 겹이 더 있다 — 수가 커지면 이웃 사이 간격도 커지므로(아래 사건 3), 고정된 𝜀 대신 수의 크기에 비례하는 허용치(상대 오차)를 쓰는 것이 안전하다. 두 방식의 구체적인 사용법과 함정은 50장에서 C 코드로 다룬다. 지금 기억할 것은 한 문장이다: 부동소수점을 ==로 비교하는 코드는 거의 언제나 의심 대상이다.

사건 3 — 큰 수 곁에서 작은 수는 사라진다. 유효숫자가 유한하다는 것은, 크기가 너무 다른 두 수를 한 그릇에서 만나게 하면 작은 쪽이 통째로 사라진다는 뜻이기도 하다. 64비트 double(유효 십진 약 15–16자리) 에서 가장 깔끔하게 드러나는 자리가 1016 근처다.

세 사건의 뿌리는 하나다 — 표현 가능한 수들이 띄엄띄엄한 눈금이고, 눈금 간격은 수가 클수록 넓어진다는 것. 0 근처에서는 눈금이 촘촘해 10300 같은 수도 구별하지만, 방금 본 대로 1016쯤 가면 눈금 간격이 1을 넘어 정수조차 건너뛴다. “몇 자리까지 믿을 수 있는가”(유효숫자)라는 감각이 부동소수점 사용의 전부라 해도 과언이 아니다.

8.5 난립, 그리고 IEEE 754라는 계약

부동소수점이라는 착상은 일찍부터 있었지만, 어떻게 담을 것인가 — 가수와 지수에 몇 비트씩 줄 것인가, 반올림은 어느 쪽으로 할 것인가 — 는 오랫동안 회사마다 달랐다. IBM 대형기, DEC의 VAX, Cray 슈퍼컴퓨터가 저마다 다른 형식을 썼다. 같은 프로그램이 기계를 옮기면 다른 답을 내놓았고, 수치 계산 프로그래머들은 기계마다 계산의 버릇을 새로 익혀야 했다.

1985년, 이 난립을 IEEE 754라는 표준이 통일했다. 형식(부호 1비트 + 지수 + 가수), 반올림 규칙, 무한대와 “수 아님”(NaN) 같은 특수값의 처리까지 못박은 정밀한 계약이다. 오늘날 사실상 모든 CPU와 GPU가 이 계약을 따른다 — 32비트 형식(C의 float)과 64비트 형식(C의 double)이 그 대표다.

시대를 눈여겨보면 재미있다. 부동소수점의 계약(IEEE 754, 1985)과 C 언어의 계약(ANSI C, 1989)은 같은 몇 해 사이에 태어났다. 벤더마다 제멋대로이던 것을 견디다 못해 산업 전체가 “계약서를 쓰자”로 돌아선 시대였다 — 이 “계약의 시대” 이야기는 12장에서 다시 만난다.

문. 32비트 float와 64비트 double은 실무에서 어떻게 갈라 쓰는가?

답. 기본값은 double이다. 64비트 형식은 십진 유효숫자로 약 15–16자리를 감당해서 대부분의 계산에 여유가 있다. float(약 7자리)는 메모리와 대역폭이 아쉬운 곳 — 대량의 좌표, 그래픽, 기계학습 — 에서 크기의 이득으로 선택한다. “정밀도가 필요해서 double, 물량이 필요해서 float” 라고 기억하면 대강 맞다. C에서의 실제 사용은 50장에서 다룬다.

문. 7장의 정수 오버플로와 이 장의 반올림은 둘 다 “유한한 그릇” 문제인데, 성격이 같은 문제인가?

답. 뿌리는 같고 증상이 다르다. 정수의 그릇은 범위가 유한해서 끝에서 넘치고(오버플로 — 넘치기 전까지는 완전히 정확하다), 부동소수점의 그릇은 정밀도가 유한해서 어디서나 조금씩 어림된다(대신 범위는 어마어마하다). 정확하지만 좁은 그릇과, 넓지만 어림하는 그릇 — 어느 그릇을 쓸지 고르는 감각이 수를 다루는 기본기이고, C의 타입 선택 (27장, 50장)이 바로 그 고르기다.

표현의 사다리 첫 단을 정리하면 이렇다. 소수점은 비트에 없고 약속에 있다. 약속을 못박으면 고정소수점, 데이터에 실으면 부동소수점이고, 부동소수점의 약속은 IEEE 754라는 계약으로 통일되어 있다. 그리고 그 계약 아래의 수는 정확한 수가 아니라 성실한 근사다.

다음 단은 글자다. 사물함에 “안녕”을 담으려면 무엇을 약속해야 하는가 — 문자와 텍스트의 세계, 그리고 그 약속들이 어긋나며 남긴 흉터들의 이야기다.