Skip to content

2부. 세는 세계와 재는 세계

앞 장에서 우리는 왜 컴퓨터 안에 계산기가 둘인지, 그리고 FPU가 왜 하나의 타협처럼 등장했는지를 봤다. 이제 한 단계 더 들어가서, 다음으로 미뤄두었던 질문을 다시 꺼내본다.

부동소수점의 한계는 회로의 정교함 너머에 있는가? 그렇다면 그 뿌리는 어디에 있는가?

먼저 우리가 이미 일상에서 쓰는 직관적인 구분부터 다시 잡아본다. 사과 5개, 칩 32개, 셀 128개처럼 몇 개인지를 말할 때 우리는 숫자를 센다. 길이 1.73미터, 속도 초속 3.2미터, 온도 22.4도처럼 연속된 양의 크기를 말할 때 우리는 숫자를 잰다. 이건 단어의 차이를 넘어선 자리에 있다 — 빈 공간이 있는 이산(Discrete) 세계와 빈틈없이 이어지는 연속(Continuous) 세계의 차이. 정수를 다루는 이산 세계에서 34 사이는 그대로 비어 있다. 실수를 다루는 연속 세계에선, 3.04.0 사이에 3.1, 3.14, 3.1415처럼 무한정 값이 끼어 들어간다. 이 텅 비어있음무한히 끼어듦의 차이가 바로 오차의 뿌리가 된다.

세는 세계 Discrete · 정수의 자리 1 2 3 4 5 6 정수와 정수 사이는 그대로 비어 있다 재는 세계 Continuous · 실수의 자리 π 3.14159 26535 89793 23846 ... — 끝까지 이어진다

인간은 왜 재는 언어를 택했는가

Section titled “인간은 왜 재는 언어를 택했는가”

우리가 사는 세계는 대개 세는 것보다 재는 것을 선호한다. 물체는 움직이고, 온도는 변하고, 압력은 오르내리고, 전압은 흔들린다. 이런 현상은 매끈한 곡선과 변화율로 표현할 때 훨씬 이해하기 쉽다.

그래서 인간은 오랫동안 연속적인 수학의 언어를 발전시켜 왔다. 미적분이 강력한 이유도 여기에 있다. 미분과 적분은 변화와 누적을 하나의 언어 안에서 매끈하게 다룰 수 있게 해 준다.

연속적인 실수의 언어는 종이 위에서는 완벽하게 작동했다. 문제는 이 매끄러운 곡선의 언어를 컴퓨터라는 딱딱한 기계 속에 집어넣는 순간 발생한다.

컴퓨터는 끝까지 이산적인 기계다

Section titled “컴퓨터는 끝까지 이산적인 기계다”

컴퓨터 안의 값은 끝까지 유한한 자리에 머문다. 레지스터도 메모리도 결국 유한한 저장 공간의 묶음이다. 상태는 끝까지 0과 1의 조합으로 저장된다. 이 점에서 컴퓨터는 본질적으로 이산적인 기계다.

그래서 컴퓨터가 실수를 다룬다는 말은 정확히 말하면 이런 뜻이다.

연속적인 것처럼 보이는 표현을 유한한 비트에 근사해서 올려놓는다.

앞 장에서 살펴본 ALU와 FPU의 분리도 정확히 이 맥락에 있다. ALU는 뚝뚝 끊어진 0과 1의 비트 세계와 자연스럽게 맞물린다. 한편 FPU는 그 끊어진 기계 위에 끊어지지 않는 수를 흉내 내기 위한 무거운 번역기를 하나 더 얹는다. 이 번역 과정에서 발생하는 손실이 바로 FPU가 가진 구조적 한계다.

이 말을 가장 짧게 보여주는 예가 0.1이다.

10진수에서 0.1은 너무 익숙해서 딱 떨어지는 값처럼 느껴진다. 이 값을 컴퓨터가 다루는 이진수로 옮기면 끝까지 이어진다. 컴퓨터는 2를 곱하고 나누는 방식으로 수를 저장하는데, 1/10은 그 방식으로 옮기면 끝없이 흘러가기 때문이다. 그래서 컴퓨터는 0.1을 저장할 때, 정확한 0.1에 가장 가까운 어떤 근사값을 골라 담는다. 예를 들어 float에서는 대략 이렇게 저장된다.

0.1(1.10011001100110011001101)2×240.10000000149011612\begin{aligned} 0.1 &\approx \left(1.10011001100110011001101\right)_2 \times 2^{-4} \\ &\approx 0.10000000149011612 \end{aligned}

이 사실 하나로 이미 중요한 결론이 나온다. 부동소수점은 처음부터 비슷한 근사값에서 출발하는 그릇이라는 점이다. 오차는 숫자를 기계에 올리는 첫 순간부터 조용히 들어와 있다.

오차는 왜 누적되고, 왜 순서에 민감한가

Section titled “오차는 왜 누적되고, 왜 순서에 민감한가”

근사값을 가지고 계산하면 그 다음 연산도 근사값 위에서 진행된다. 여기서 여러 문제가 구조적으로 따라온다. 처음부터 근사값으로 출발했기 때문에, 한 번의 작은 반올림 오차도 수만 번의 반복 계산을 거치며 눈덩이처럼 불어난다.

더 치명적인 것은 순서다. 동시에 하는 계산에서 더하는 순서나 병렬 처리 타이밍이 살짝 비틀어져도 중간 반올림 지점이 달라진다. 수학적으로는 완벽히 똑같은 식인데도, 돌릴 때마다 결과가 요동치는 재현성 문제가 여기서 발생한다. 이 현상은 계산의 바탕이 근사값과 반올림이라는 한 가지 사실에서 자라난다.

부동소수점이 주는 편리함 뒤에는, 오차를 계속 추적하고 관리하는 구조적 비용이 따라붙는다.

이건 기술의 정교함 너머의 문제다

Section titled “이건 기술의 정교함 너머의 문제다”

여기서 흔히 나오는 반응이 있다. 비트폭(저장 공간)을 더 넓히면 되는 것 아닌가? 회로를 더 정교하게 만들면 되는 것 아닌가?

물론 정밀도를 높이면 상황은 나아진다. 더 많은 비트를 쓰면 근사 오차를 더 작게 만들 수 있다. 다만 그건 작아진다까지의 이야기다. 사라진다는 자리는 그 너머에 있다.

연속적인 값을 유한한 공간에 담으려면 값과 값 사이에 빈틈이 생긴다. 결국 어떤 수는 근사값으로 욱여넣어야 하고, 그렇게 욱여넣은 근사값끼리 계산한 결과는 꼬리에 꼬리를 무는 오차로 이어진다. 부동소수점의 한계는 구현의 정교함 너머에서 자라난다. 끊어지지 않는 크기유한하고 끊어진 기계에 표현하는 과정에서 따라오는 정밀도의 손실이다.

현대 문명을 일군 실수의 가치는 그대로 살아 있다. 이 장의 끝에서 던지고 싶은 질문은 한 가지다.

실수를 기계에 욱여넣으며 발생하는 이 거대한 오차들을, 우리는 언제까지 계산의 어쩔 수 없는 기본값으로 받아들여야 하는가?