Programming/Interview

0.1 + 1.1 != 1.2

JKROH 2024. 1. 18. 23:42
반응형

 제목에 쓰여진 수식은 참이다. 최근 면접에서 왜 저 수식이 참인지를 질문 받았었다. 아주 예전에 코딩애플님 유튜브 영상에서 봤었는데, 너무 당연한 거라고 생각해서 그냥 재밌게 보고 말았었다. 문제는 면접 때 왜 당연한지 기억을 못해냈다는건데...

 

 게임 내 화폐나 비트코인들이 실물 화폐와 비슷한 혹은 그 이상의 가치를 지니는 경우가 종종 있다. 이런 경우를 보고 조롱 섞인, 혹은 걱정 어린 목소리를 내는 사람들이 주로 '그래서 10100110이 그렇게 비싸다 이거지?'와 비슷한 결의 말들을 하곤 한다. 이는 컴퓨터가 어떻게 데이터를 저장하는가에 대한 정확한 이해를 기반으로 하는 말이라고 할 수 있다.

 

 컴퓨터 내의 데이터는 모두 0또는 1로 이루어져 있다. 이는 컴퓨터가 비트 단위 연산을 하기 때문이다. 비트코인이건, 집행검이건, 결국 최종적으로 까보면 0과 1로 이루어진 데이터일 뿐이다. 컴퓨터 내에서 0과 1을 조합해 비트코인도 만들고 집행검도 만든다.

 

 예를 들어, 11이라는 숫자를 0또는 1로 만들려면 어떻게 해야할까? 진법의 변화를 이용하면 된다. 앞서 이야기한 11은 10진법을 사용했을 때 11이다. 만일 11진법을 사용했다면 11은 10(11)이 될 것이다. 마찬가지로, 0과 1을 사용하는 컴퓨터에게 11을 이해시켜주기 위해선 11을 2진수로 바꿔주면 된다. 즉, 11 = 1011(2)가 된다.

 

 모든 정수는 2진수로 바꿔줄 수 있다. 그에 맞는 메모리 공간만 할당해 줄 수 있다면 말이다. 우리가 흔히 정수타입으로 사용하는 int와 long을 살펴보면 각각 32비트, 64비트의 메모리 공간을 차지하여 정수를 표현한다. 이 때, 비트의 맨 앞자리에 사용되는 0또는 1은 양수, 음수를 나타낸다. 그래서 int타입은 32개의 비트가 있음에도 2^32-1이 아니라 -(2^31) ~ 2^31-1 까지 표현이 가능해진다. 문자열은 각 비트값에 해당하는 문자들을 저장하고 사용한다.

 

 그래서 정수는 그렇다 치는데, 소수는 그럼 어떻게 처리해야할까? 예를 들어 5.5를 메모리에 쓴다고 생각해보자. 5.5를 이진수로 변환하면 101.1이 나온다. 소수의 경우 1/2, 1/4, 1/8 ... 과 같이 1/2^n의 형태로 표현할 수 있는데, 0.5는 0.1, 0.25는 0.01, 0.125는 0.001이라고 생각하면 쉽다. 그럼 0.75는 뭘까? 0.25 + 0.5이기 때문에 0.11이 된다.

 

 아무튼 그럼 101.1을 그대로 메모리에 쓰냐. 그건 아니다. 101.1은 다시 1.011 * 2^2의 형태로 변환되어 메모리에 저장된다. 이를 부동 소수점 방식이라고 한다. 

 

 위의 그림은 자바의 float에서 사용하는 부동 소수의 비트 배분이다. float 타입은 32비트를 차지하는데, 이 때 맨 앞 비트는 마찬가지로 숫자의 음 / 양을 나타낸다. 다음 8비트는 지수 비트로 2^n에서 n에 해당하는 값을 이진화해 적어넣는다. 1.011 * 2^2 이면 지수 비트에 (127 + 2)를 한 10000001이 적힌다(이를 Bias표기법이라 한다.). 나머지 가수 비트는 011이 고대로 들어간다.

 

 그런데, 0.1을 이진화하면 순환 소수가 나온다. 0.0001100110011001100....이 반복된다. 그럼 얘를 저장하기 위해서는 무한한 메모리 공간이 필요하다는 건데, 무한한 메모리 공간이 현존한다면 개발자들이 가비지 컬렉팅을 공부하고 있을 일도 없었을 것이며, 우리는 좋은 램에 더 비싼 값을 주지도 않았을 것이다. 결국 0.1을 저장하기 위해서는 주어진 메모리 공간 내에서 비트를 잘라내 저장해야한다. 이는 곧 정확하게 0.1이 저장되지 않음을 의미한다. 따라서 0.1 + 1.1 == 1.2가 될 수 없다. 0.1 + 1.1

 

반응형