10진수를 1이 될 때까지 계속 2로 나눠가면서 나머지를 구하고, 밑에서부터 거꾸로 읽으면 된다.
예를 들어 10진수 35는 2진수로 바꾸면 100011이다.
정수는 이런데, 이 글의 주제는 부동 소수점이다. 부동소수점은 뒤에서도 언급하겠지만 컴퓨터에서 실수 (real number)를 표현할 때 쓰는 방법이다.
실수, 그러니까 소수점이 붙어있는 수는 어떻게 2진수로 변환할까?
일단 소수점 앞부분, 그러니까 정수부는 그냥 정수 변환하는 거랑 똑같이 하면 된다.
소수부가 문제인데, 얼핏 생각하면 소수점 뒤에 있는 숫자들을 하나씩 2진수로 바꿔버리면 되지 않냐고 생각할 수도 있지만,
그렇게 하면 아래 예시처럼 서로 다른
10진수 숫자가 2진수로 변환되었을 때 중복이 되는 문제가 있다. (정수부도 그렇게 10진수 자리별로 쪼개서 2진수로 바꿔버리면 같은 문제가 생기기 때문에 그러면 안된다.)
ex)
1.9→1.1001
1.41 →1.1001
사실 그냥 정수부 변환의 정반대로 하면 된다. 즉, 정수부에선 10진수를 2로 나눠가면서 1이나 0을 뽑았다면, 소수부는 10진수에 2를 곱해가면서 1이나 0을 뽑아낸다.
그리고 정수부 변환할 때는 1이 나올 때 종료했다면 여기서는 0이 나오면 종료하고, 결과를 밑에서부터가 아니라 위에서부터 읽어준다.
0.625를 이진수로 변환하는 예시를 보자.
0.625*2 = 1.25 →1을 빼내고 나머지 0.25
0.25*2 =0.5 → 0을 빼내고 나머지 0.5
0.5*2 = 1.0 → 1을 빼내고 나머지 0 (여기서 0이 나왔으니 종료)
나머지 이 나왔으니 변환을 종료하고 빼낸 숫자들을 위에서부터 읽어주면 된다.
즉 0.625는 →0.101이 된다.
직관적으로 알 수 있는 사실은 저 숫자가 n/(2의 배수) 꼴의 분수로 예쁘게 표현되는 숫자일수록 2진수로 바꿨을 때 자릿수가 적고, 그렇지 않을수록 자릿수가 늘어날 것이라는 사실이다.
0.5, 0.25, 0.125, 0.75 같이 흔히 나누기하다 보면 자주 보게 되는 숫자들이 변환하기 편하고, 0.789 같이 10진수 기준으로는 자릿수가 길지 않더라도 딱 봐도 지저분해 보이는 숫자면 2진수로 바꾸면 엄청나게 길이가 늘어날 것이다.
고정소수점(Fixed Point) 표현방식
고정소수점 표현 방식이라는 것은 쉽게 말해 위에서 설명한 방법대로 10진수를 2진수로 바꿨으면, 그걸 그대로 박아 넣는 방식이다.
예를 들면 7.625라는 실수가 있다고 치자. 2진수로 변하면 111.101이 될 것이다. 이걸 이렇게 저장한다.
16비트 체계를 쓴다고 가정하자.
맨 앞 1자리는 부호 비트 (Sign Bit)라고 해서 0이면 양수, 1이면 음수라는 뜻이다.
나머지 비트들은 소수점을 기준으로 정수부랑 소수부를 표현하는 비트로 각각 나누게 되는데, 소수점의 위치는 미리 정해둔다.
소수부의 경우 앞에서부터 채우며 남는 뒷자리는 다 0으로 채운다.
이러한 고정소수점 방식은 구현하기 편리하지만 사용하는 비트 수 대비 표현 가능한 수의 범위 또는 정밀도가 낮기 때문에 실수를 다룰 필요가 있는 범용 시스템에서는 거의 안 쓰이고, 높은 정밀도가 필요 없는 소규모 시스템에서는 간혹 쓰이기도 한다.
부동소수점(Floating Point) 표현 방식
부동소수점 표현 방식에서는 2진수로 변환한 결과를 그대로 박아 넣지 않고 몇 가지 추가로 거친다.
-정규화(Normalization)
정규화라는 단어는 수학이나 컴퓨터 분야에서 다양한 의미로 쓰이지만 여기서 말하는 정규화라는 것은 2진수를
1.xxxx...*2^n
꼴로 변환하는 것을 말한다.
변환하는 방법은 간단한데, 정수부에 1만 남을 때까지 소수점을 왼쪽(*정수부가 0일 경우엔 오른쪽. 아래에서 다시 설명)으로 이동시키고, 이동한 칸 수만큼 n자리에 집어넣으면 된다.
예를 들어서 위에서 봤던 111.101를 정규화하면 1.11101 * 2^2이 된다. (2^2는 십진수로 100이다)
여기서 소수점을 '이동'시킨다는 데서 '부동' 소수점, floating point라는 용어가 나온 것이 아닌가 싶다.
-IEEE 754 부동소수점 표현
IEEE 754 : IEEE(미국의 전기 전자 기술자 협회)에서 개발한 컴퓨터에서 부동소수점을 표현하는 가장 널리 쓰이는 표준이다.
IEEE 표준에 따르면 부동소수점 방식으로 실수를 저장하는 데는 32비트 또는 64비트가 사용되며 32비트 기준으로 아래 그림과 같은 구조를 가진다.
7.625라는 숫자를 이 방식으로 표현해보자.
부호 비트는 고정소수점과 마찬가지로 0이면 양수, 1이면 음수를 의미한다.
23자리 가수부는 정규화 결과 소수점 오른쪽에 있는 숫자들을 왼쪽부터 그대로 넣으면 된다. 남는 자리는 0으로 채운다.
(참고 : 소수점 왼쪽은 정규화를 하면 무조건 1이기 때문에 신경 쓰지 않고 표현도 안 하는데, 이 1을 hidden bit라고 부르기도 한다. ex 1.11101에서 맨 앞에 1)
남은 건 8자리짜리 지수부인데, 일단 '지수'부라는 이름으로 봤을 땐 2^n에서 n에 해당하는 수, 그러니까 예시에서는 2를 2진수로 바꾼 '10'을 넣으면 될 것 같다.
근데 IEEE 표준에 따르면 저 부분에는 지수를 그대로 박아 넣는 게 아니라, 'bias'라고 하는 지정된 숫자를 더한 다음 넣어야 한다.
bias : IEEE 754 표준에 있는 상수값(127) 지수에 따라 상수값이 바뀐다. 이 것을 쓰는 이유는 지수가 음수가 될 수도 있기 때문
IEEE 표준에서 32비트를 쓰는 경우 bias는 127이라고 규정하고 있다. 따라서 2(지수값 2^2=4 즉 , 10 ) + 127 = 129를 2진수로 바꾼 10000001이 들어간다.
결론적으로 7.625는 컴퓨터에서 아래와 같이 저장된다.
이 bias라는 값을 왜 쓰냐면, 지수가 음수가 될 수도 있어서 그렇다. 예를 들면,
0.000101이라는 이진수가 있다 치면, 정규화에 대해서 설명할 때 정수부를 1로 만들어야 한다. 그러니까 왼쪽이 아니라
오른쪽으로 소수점을 밀어서 1.01*2^-4가 된다.
만약 bias가 없어서 위에서 2를 그냥 00000010으로 저장했다고 생각해보자. -4는 어떻게 저장할까?
부호 비트는 지수의 부호를 뜻하는 게 아니라 전체 숫자의 부호를 뜻하는 거라서 이거랑 상관이 없다.
그렇다고 지수용 부호 비트를 하나 더 만들자니 이것대로 복잡하다.
그래서 8자리를 가지고 음수랑 양수를 둘 다 표현하자니,
(10진수 기준으로) 0~127구간은 음수, 128~255 구간은 양수를 표현하도록 만든 것이다. (그래서 127을 더한 거다)
(참고: 실제로는 0이랑 255는 0이나 0에 한없이 수렴하는 작은 수들, 무한대, NaN -Not a Number- 같은 걸 표현하기 위해서 특별하게 지정되어 있기 때문에 일반적인 표현 범위에 포함되지 않으며, 저런 수들을 표현할 때는 이 글에서 설명한 정규화 방법이 적용되지 않는다.)
위 그림에서 살펴본 32비트 체계를 32비트 단정도(Single-Precision), 64비트 체계를 64비트 배정도(Double-Precision)이라고 부른다.
그리고 프로그래밍 언어를 다뤄봤다면 흔히 접할 수 있는 실수형 타입 float, double이 각각 전자(32), 후자(64)에 해당한다. float은 부동소수점 방식을 사용하는 기본형이라는 의미로 floating point에서 따왔고, double은 64비트 배정도를 사용한다는 의미로 double-precision에서 따왔을 것이다.
double, 그러니까 64비트 체계에서는 지수부가 11비트, 가수부가 53비트다.
지수부가 2^11 즉,
2048개의 수를 표현할 수 있으므로, 0~1023 구간은 음수, 1024~2047 구간은 양수 지수를 의미하며 bias는 1023이 된다.
이와 같은 부동소수점 표현 방식은 위에서 살펴본 고정소수점 표현 방식에 비해서 비트 수 대비 표현 가능한 수의 범위와 정밀도 측면에서 보다 우위에 있기 때문에, 정규화니 bias니 하는 복잡한 과정이 들어감에도 불구하고, 현재 대부분의 컴퓨터 시스템에서 부동소수점을 이용해 실수를 표현하고 있다.