티스토리 뷰

어제 트위터 타임라인을 보다가, '부동소수점' 이라는 단어를 보면 어떻게 해석하게 되느냐는 그런 트윗이 RT로 넘어왔다.


생각해보니까 굉장히 비직관적인 단어다.

원래 영어단어는 floating point인데, 소수점 (point) 이 둥둥 떠다닌다 (floating) 고 써놔도 뭔 소린지 단번에 이해가 안되는데, 이걸 또 '부동' (浮動) 이라고 번역을 해놨다.


문제는 한국어에서 그냥 '부동' 이라고 하면 저것보다 더 높은 빈도로 쓰이는 다른 단어들이 많다는 것이다. 

不動 (움직이지 않음. '부동자세' 의 부동) 이나 不凍 (얼지 않음. '부동항' 의 부동), 不同 (같지 않음. '표리부동' 의 부동) 등등. '부동소수점' 이

라는 단어만 딱 보고 저 부동이 움직인다는 뜻이라는 걸 단번에 알아챌 사람은 아마 거의 없으리라.


여튼, 이 얘기를 하다보니까 왜 소수점이 움직인다는 의미의 '부동소수점' 이란 이름이 붙었는지 옛날 학부 때 컴퓨터 아키텍처 시간에 배웠던 내용을 한번 더듬어보고 싶어졌다. 영 기억이 안나서 다시 좀 찾아봤고, 그 내용을 정리해서 포스팅하려 한다.




1) 이진기수법


컴퓨터는 0과 1로 이루어진 기계어를 사용한다는 사실은 널리 알려져 있다. 인간은 수를 표현할 때 기본적으로 10진법을 사용하지만, 컴퓨터는 이를 0과 1 그러니까 2진법으로 저장하게 된다. 

그러므로 컴퓨터가 수를 표현하는 법에 대해서 얘기하려면 10진수를 2진수로 바꾸는 방법에 대해서 알아둘 필요가 있다.


정수의 경우는 간단한 편이고 2진수라는 걸 접해봤다면 대부분 알 내용이다.

0, 1, 10, 11, 100, 101, 110, 111, 1000, ... 이런 식으로, 10진수 기준으로 2가 나올 차례가 되면 2를 쓰는 대신 (2진수는 0, 1만 사용하는 체계이므로) 자릿수를 늘려주는 것이다.


10진수에서는 10^n에 해당하는 수가 될 때마다 자릿수가 올라갔다면, 2진수에서는 2^n에 해당하는 수가 될 때마다 자릿수가 올라간다.

10진수 4는 2진수로 100, 8 (2^3) 은 1000, 16 (2^4) 은 10000, 32는 100000인 식이다. 


좀 더 일반화시켜서 10진수 정수를 2진수로 바꾸는 방법을 설명하면 그림과 같다. 

10진수를 1이 될 때까지 계속 2로 나눠가면서 나머지를 구하고, 밑에서부터 거꾸로 읽으면 된다.

예를 들어 10진수 35는 2진수로 바꾸면 100011이다.


정수는 이런데, 이 글의 주제는 부동소수점이다. 부동소수점은 뒤에서도 언급하겠지만 컴퓨터에서 실수 (real number) 를 표현할 때 쓰는 방법이다. 실수, 그러니까 소수점이 붙어있는 수는 어떻게 2진수로 변환할까?


일단 소수점 앞부분, 그러니까 정수부는 그냥 정수 변환하는 거랑 똑같이 하면 된다.

소수부가 문제인데, 얼핏 생각하면 소수점 뒤에 있는 숫자들을 하나씩 2진수로 바꿔버리면 되지 않냐고 생각할 수도 있지만 그렇게 하면 아래 예시처럼 서로 다른 10진수 숫자가 2진수로 변환되었을 때 중복이 되는 문제가 있다. (정수부도 그렇게 10진수 자리별로 쪼개서 2진수로 바꿔버리면 같은 문제가 생기기 때문에 그러면 안된다)


1.9 -> 1.1001

1.41 -> 1.100 1


사실 그냥 정수부 변환의 정반대로 하면 된다. 즉 정수부에선 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진수로 바꾸면 엄청나게 길이가 늘어날 것이다.


2) 고정소수점 (Fixed Point) 표현 방식


고정소수점 표현 방식이라는 것은 쉽게 말해 위에서 설명한 방법대로 10진수를 2진수로 바꿨으면, 그걸 그대로 박아넣는 방식이다.


예를 들면 7.625라는 실수가 있다고 치자. 2진수로 변환하면 111.101이 될 것이다. 이걸 이렇게 저장한다.


16비트 체계를 쓴다고 가정하자.

맨 앞 1자리는 부호 비트 (Sign Bit) 라고 해서 0이면 양수, 1이면 음수라는 뜻이다.

나머지 비트들은 소수점을 기준으로 정수부랑 소수부를 표현하는 비트로 각각 나누게 되는데, 소수점의 위치는 미리 정해둔다.

소수부의 경우 앞에서부터 채우며 남는 뒷자리는 다 0으로 채운다.


이러한 고정소수점 방식은 구현하기 편리하지만 사용하는 비트 수 대비 표현 가능한 수의 범위 또는 정밀도가 낮기 때문에 실수를 다룰 필요가 있는 범용 시스템에서는 거의 안 쓰이고, 높은 정밀도가 필요없는 소규모 시스템에서는 간혹 쓰이기도 한다고 한다.


3) 부동소수점 (Floating Point) 표현 방식


부동소수점 표현 방식에서는 2진수로 변환한 결과를 그대로 박아넣지 않고 몇 가지 과정을 추가로 거친다.


- 정규화 (Normalization)


정규화라는 단어는 수학이나 컴퓨터 분야에서 다양한 의미로 쓰이지만 여기서 말하는 정규화라는 것은 2진수를 


1.xxxx... * 2^n


꼴로 변환하는 것을 말한다. 


변환하는 방법은 간단한데, 정수부에 1만 남을 때까지 소수점을 왼쪽 (*정수부가 0일 경우엔 오른쪽. 아래에서 다시 설명) 으로 이동시키고 이동한 칸 수만큼 n 자리에 집어넣으면 된다.

예를 들어서 위에서 봤던 111.101을 정규화하면 1.11101 * 2^2가 된다.


여기서 소수점을 '이동' 시킨다는 데서 '부동' 소수점, floating point라는 용어가 나온 것이 아닌가 싶다.


- IEEE 754 부동소수점 표현


IEEE 표준에 따르면 부동소수점 방식으로 실수를 저장하는 데는 32비트, 또는 64비트가 사용되며 32비트 기준으로 아래 그림과 같은 구조를 가진다.



부호 비트는 고정소수점에서와 마찬가지로 0이면 양수, 1이면 음수를 의미한다.

23자리 가수부는 정규화 결과 소수점 오른쪽에 있는 숫자들을 왼쪽부터 그대로 넣으면 된다. 남는 자리는 0으로 채운다.

(참고: 소수점 왼쪽은 정규화를 하면 무조건 1이기 때문에 신경쓰지 않고 표현도 안 하는데, 이 1을 hidden bit라고 부르기도 한다)


남은건 8자리짜리 지수부인데, 일단 '지수' 부라는 이름으로 봤을 땐 2^n에서 n에 해당하는 수, 그러니까 예시에서는 2를 2진수로 바꾼 '10' 을 넣으면 될 것 같다.

근데 IEEE 표준에 따르면 저 부분에는 지수를 그대로 박아넣는게 아니라, 'bias' 라고 하는 지정된 숫자를 더한 다음 넣어야 한다.


IEEE 표준에서 32비트를 쓰는 경우 bias는 127이라고 규정하고 있다. 따라서 2 + 127 = 129를 2진수로 바꾼 10000001이 들어간다.

결론적으로 7.625는 컴퓨터에서 아래와 같이 저장된다.



이 bias라는 값을 왜 쓰냐면, 지수가 음수가 될 수도 있어서 그렇다.

예를 들면 0.000101이라는 이진수가 있다 치자. 정규화에 대해서 설명할 때 정수부를 1로 만들어야 한다고 했다. 그러니까 왼쪽이 아니라 오른쪽으로 소수점을 밀어서 1.01 * 2^-4가 된다.


만약에 bias가 없어서 위에서 2를 그냥 00000010으로 저장했다고 생각해보자. -4는 어떻게 저장할래?

부호 비트는 지수의 부호를 뜻하는게 아니라 전체 숫자의 부호를 뜻하는 거라서 이거랑 상관이 없다.

그렇다고 지수용 부호 비트를 하나 더 만들자니 이것대로 복잡하다. 그래서 8자리를 가지고 음수랑 양수를 둘 다 표현하자니, (10진수 기준으로) 0~127 구간은 음수, 128~255 구간은 양수를 표현하도록 만든 것이다.


(참고: 실제로는 0이랑 255는 0이나 0에 한없이 수렴하는 작은 수들, 무한대, NaN -Not a Number- 같은 걸 표현하기 위해서 특별하게 지정되어 있기 때문에 일반적인 표현 범위에 포함되지 않으며, 저런 수들을 표현할 때는 이 글에서 설명한 정규화 방법이 적용되지 않는다)


위 그림에서 살펴본 32비트 체계를 32비트 단정도 (Single-Precision), 64비트 체계를 64비트 배정도 (Double-Precision) 이라고 부른다. 

그리고 프로그래밍 언어를 다뤄봤다면 흔히 접할 수 있는 실수형 타입 float, double이 각각 전자, 후자에 해당한다. float는 부동소수점 방식을 사용하는 기본형이라는 의미로 floating point에서 따왔고, double은 64비트 배정도를 사용한다는 의미로 double-precision에서 따왔을 것이다.


double, 그러니까 64비트 체계에서는 지수부가 11비트, 가수부가 52비트다. 지수부가 2^11 즉 2048개의 수를 표현할 수 있으므로 0~1023 구간은 음수, 1024~2047 구간은 양수 지수를 의미하며 bias는 1023이 된다.


이와 같은 부동소수점 표현 방식은 위에서 살펴본 고정소수점 표현 방식에 비해서 비트 수 대비 표현 가능한 수의 범위와 정밀도 측면에서 보다 우위에 있기 때문에, 정규화니 bias니 하는 복잡한 과정이 들어감에도 불구하고 현재 대부분의 컴퓨터 시스템에서 부동소수점을 이용해 실수를 표현하고 있다.


공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함