정수와 같은 방법으로 실수를 한번 저장한다고 가정해보자.
자 그러면 일단은 첫 번째 비트는 부호를 저장하는데 이때 양수면 0으로 음수면 1로 저장을 하게 된다.
그리고 실수라는 건 정수부와 소수부로 나누어지는 수를 말한다.
그럼 정수부는 정수부대로 저장을 하고 소수부는 소수부대로 저장을 하게 되면 정확하게 10.31이라는 값을 저장할 수 있다.
그런데 예를 들어 10.000031 이런 실수를 저장한다고 한번 해 보자.
그러면 10은 정수를 저장하는 데는 전혀 무리가 없다. 그런데 소수부를 저장한다고 생각을 했을 때 소수부는 31을 저장하려고 봤더니 .000031이랑은 다르다. 즉 소수부는 각 소수부 한자리 씩 따로따로 값을 저장을 해 줘야 된다는 결론이 나오게 된다.
그러면 소수부 한자리에 들어갈 수 있는 수의 범위는 0부터 9 사이의 범위가 들어갈 수가 있게 되는데,
0부터 그 사이의 범위를 저장하기 위해서는 소수부 한자리당 최소 4비트씩을 필요로 한다는 뜻이 된다.
그럼 실수라는 건 10.000031 와 같이 아주 큰 수나 아주 작은 수까지도 표현할 수 있어야 하는데
소수부 한자리당 이렇게 4비트씩 할당하면 나중에는 메모리를 감당하기가 힘들것이다. 왜냐하면 컴퓨터의 메모리는 무한한 메모리 공간이 아니라 딱 정해진 메모리 공간에 가급적이면 효율적으로 데이터를 저장할 수 있어야 되기 때문이다.
이렇게 소수점이 딱 정해진 상태로 정수부와 소수부를 저장하는 이런 방식을 고정 소수점 방식이라고 하는데
이렇게 저장하면 정확한 값을 저장할 수 있지만, 너무 큰 수나 아주 작은 수 표현에는 부적합하다는 단점을 가지고 있다.
그래서 어떻게 하면 제한된 메모리에 좀 더 큰 수와 좀 더 작은 수를 저장할 수 있을까라는 방법을 고민하게 됐는데,
그 방법이 바로 부동소수점 방식이다.
부동소수점에서 "부"는 아닐 부(不)가 아니라 바닷가에서 둥둥 떠다니는 부표와 같은 표 부(浮)자이다.
즉 소수점이 떠다닌다는 의미의 부동인데, 부동 소수점 방식을 이용하면 소수점을 떠다니게 만들어 주는 것이다.
소수점을 떠다니게 하기 위해(소수부와 정수부를 떠다니며 넘나드는..) 정수부를 0이 아닌 한 자리로 이동시키게 되는데,
예를 들어서 123.375를 부동 소수점으로 변경한다고 했을 때, 정수부를 무조건 0이 아닌 한 자리로 바꿔 주는 것이다.
그러면 123.375가 1.23375가 되는데 이 두 수가 다르니까 같아지려면 1.233375에 10의 2승을 곱해줘야 한다.
이때 이 실수의 모양에 해당하는 부분을 가수부라고 하고, 10의 거듭승 부분을 지수부라고 부른다.
이렇게 표현하는 방식을 부동소수점 방식이라고 하는데 어떠한 실수를 이런 부동소수점 방식으로 바꾸는 과정을 "정규화 작업을 한다."라고 말한다.
다시 한번만 더 보도록 하자. 0.00456 이라는 값이 있다.
이 수를 정규화 작업을 하게되면 정수부를 무조건 0이 아닌 한 자리로 바꿔 줘야하니까 4.56이된다.
0.00456에서 세 칸이 이동했으니까 10의 마이너스 3승을 곱해 주어야 한다.
이걸 우리는 국제 표준기구인 IEEE 754의 표준이라고 하는데, 컴퓨터는 내부적으로 실수를 표현할 때 이런 부동소수점 방식, 즉 정수부를 0이 아닌 한자리로 바꾸는 방식으로 처리를 하게 된다.
그런데 지금 이해를 조금 쉽게 하기 위해서 10진수를 설명을 했지만 컴퓨터는 0과 1의 조합, 즉 0, 1로 표현을 해주어야 하니까. 결국은 정규화 작업이 2진수로 되어야한다.
자 ,그러면 실제 컴퓨터는 어떻게 처리하는지 10진수를 2진수로 한번 변환해보자.
예를 들어서 123.375라는 값이 있다. 이 10진수의 값을 2진수로 변환하면 1111011.011이 되는데 여기서 정수부를 이진수로 변환, 즉 0과 1의 조합으로 만들어 줘야 한다.
그러면 나머지 값은 당연히 0 아니면 1만 나온다. 그래서 이진수로 변환 할 때는 2로 나누면서 0이나 1의 나머지 값을 구하게 하고 8진수를 구할 때는 8로 나눈 나머지 값을 구해서 나머지가 0부터 7까지 만 나오게 하는 것이다. 왜냐하면 8로 나누었을 때 나머지는 최대 나머지 값이 7이 되는데 그러면 8진수라는 건 수를 8개 사용하겠다는 것. 즉 0부터 7까지 사용을 하겠다라는 것이다. 그러려면 8로 나눈 나머지 값으로 변환해 주면 된다는 것이다.
자, 그러면 123.375에서 정수부인 123을 2진수로 변환해 보자.
정수 123을 2진수로 변환하려면, 몫이 0이 나올 때까지 2로 계속 나누고 나머지를 기록한 다음, 위에서 아래로 나열하면 이진수가 된다.
- 123 ÷ 2 = 61, 나머지 1
- 61 ÷ 2 = 30, 나머지 1
- 30 ÷ 2 = 15, 나머지 0
- 15 ÷ 2 = 7, 나머지 1
- 7 ÷ 2 = 3, 나머지 1
- 3 ÷ 2 = 1, 나머지 1
- 1 ÷ 2 = 0, 나머지 1
나머지 값들을 역순으로 나열하면, 1111011 이 된다.
자 그러면 우리가 0.375는 어떻게 이진수로 바꿀까? 0.375를 2로 곱하고 정수 부분을 기록하면 된다.
2단의 곱셈처럼 0.375를 곱할 때는 소수점 이하의 값이 정수로 바뀌도록 한다. 이 과정을 소수 부분이 0이 나올 때까지 반복하면 된다.
- 0.375 × 2 = 0.75 → 정수부 0
- 0.75 × 2 = 1.5 → 정수부 1
- 0.5 × 2 = 1.0 → 정수부 1
각 단계에서 얻은 부분을 순서대로 나열하면 0.011 이 된다.
그래서 123.375의 2진수는 1111011.011 이 되는것이다.
이제 다음 단계인 초과 표기법에 대해서 알아보자.
초과 표기법(Excess Notation) 또는 바이어스 표기법(Bias Notation)은 지수를 표현할 때 사용하는 방식 중 하나로, 특히 부동 소수점 방식에서 사용하는 지수 표기법이다. 초과 표기법은 음수 지수 값을 양수로 표현하기 위해 사용되는데 이 방식에서는 실제 지수에 바이어스 값(bias)을 더해, 지수를 비트로 표현할 때 양수로 변환한다. 여기서 바이어스 값이란 실제 지수에 더해져 저장되는 값을 의미하는데 음수여도 지수를 모두 양수로 변환하여 저장할 수 있게 해준다.
예를 들어, IEEE 754 표준에서 단정밀도(32비트) 부동 소수점 수의 경우, 지수부는 8비트로 구성되며, 바이어스 값은 127이다. 여기에서 단정밀도는 "단순한 정밀도의 수준"을 의미하는데, 정밀도가 상대적으로 낮지만, 메모리 사용량이 효율적이라는 장점을 가지고 있다.
배정밀도(64비트) 부동 소수점 수의 경우, 지수부는 11비트로 구성되며, 바이어스 값은 1023이다.
여기에서 배정밀도는 "두 배로 증가된 정밀도의 수준" 이라는 뜻을 가지고 있으며, 32비트 단정밀도보다 더 높은 수준의 정밀도와 더 넓은 표현 범위를 가진다.
여기에서 32비트는 보통 127초과 표기법을 쓴다. 32비트에서 64 초과 표기법이 있고, 127 초과 표기법이 있는데 127 초과 표기법이라는 건 뭐냐면, 일단 기본적으로 모든 비트가 32비트에서 지수를 표현하는 부분의 비트가 127로 초기화가 되어 있으면 127 표기법이고, 64비트 같은 경우는 1023 초과 표기법이다. 지수부가 1023으로 초기화가 되어 있는 것이다.그 말은 뭐냐면, 여러분이 일단 실수를 저장하기 위한 메모리에서 127초과 표기법을 사용을 하면 지수부에 127이라는 값이 이미 저장이 되어 있다는 뜻이다.
부동 소수점 방식에서는 소수를 표현할 때 지수부와 가수부로 나누어 구분한다. 부동 소수점 방식은 실수를 표현할 때 매우 큰 수나 매우 작은 수를 효과적으로 표현하기 위해 사용되는데 가수부 (Mantissa)는 실수의 유효 숫자를 나타내는 부분이다. 일반적으로 소수점이 포함된 숫자를 정규화하여 1.xxxx의 형태로 변환한 뒤, 이 가수부에 저장이 된다.
지수부(Exponent)는 가수부의 소수점을 이동시키는 데 필요한 10의 지수를 나타낸다. 이 지수에 따라 가수부가 10의 몇 승이 되는지를 결정한다.
예를 들어 123.375라는 숫자를 단정밀도(32비트) 부동 소수점 형식으로 변환하는 정규화 작업을 해보자.
123.375의 2진수는 1111011.011이고 이를 부동 소수점 방식으로 표현하면 1.111011011 × 2^6이 된다.
1.111011011 × 2^6 에서 소수점 이하의 부분을 가수부로 사용하기 때문에 가수부는 111011011이 된다.
그 다음 단정밀도(32비트)에서 바이어스 값은 127이므로, 지수부에 저장되는 값은 6+127 = 133이된다.
133을 2진수로 변환하면 10000101 이 되는데, 이것이 지수부에 해당한다.
자, 그러면 이제 궁금한게 생긴다. 왜 얘는 6를 저장하지 않고 127을 기본적으로 저장된 상태에서 5를 저장하게 될까?
그건 바로 음수 표현을 편하게 하기 위해서이다.
무슨 말인지, 다음 예제를 한번 보자, 다음은 -0.625가 있다고 한번 해보겠다.
이진수로 변환을 할때 쉽게 가중치로 보면, 정수부는 한자리 늘어날 때마다 2씩 곱해지는 거고. 소수부는 한자리 내려올 때마다 2씩 나눠지면 되니까 6의 가중치가 0.5, 2의 가중치가 0.25, 5의 가중치는 0.125이다.
- 0.625 × 2 = 1.25 → 정수부 1
- 0.25 × 2 = 0.5 → 정수부 0
- 0.5 × 2 = 1.0 → 정수부 1
이렇게 -0.625를 2진수로 변환하면 -0.101이 된다. 그러면 얘를 부동소수점으로 변환을 하면 1.01 × 2^-1 이 되는데,
바이어스 값 127이 지수부에 들어가 있으니 -1을 빼면 126이 되고 이를 2진수로 표현하면 01111110이 되는데 이게 지수부가 된다.
그리고 가수부는 소수점 이하 모양 "01"을 그대로 저장해 주면 된다. 1은 고정되어 있으니 안쓴다.
가수부는 총 23비트로 구성되므로, 부족한 부분은 모두 0으로 채워주면 최종적으로 가수부는 01000000000000000000000이 된다.
이렇게 저장된 값을 공식으로 표현하면 다음과 같다.
즉, 바이어스 127 기준으로 이러한 공식으로 뽑아내면 되는데, m은 1.01이 되는 것이고. 지수부는 126 - 127 하면 -1이 된다.
정리하자면 우리가 지수부를 사실 음수까지도 표현할 수 있어야 되는데, 음수를 2의 보수법으로 표현하면 너무 복잡하니까 그냥 기본값으로 127을 무조건 초과하게 만드는 바이어스 127을 사용하면 지수부는 -128부터 +127까지 표현할 수 있다.
그래서 사실은 실수는 내부적으로 이런 복잡한 연산을 통해서 실수를 저장하게 된다. 왜냐하면 컴퓨터는 본래 성수만을 저장하기 위해 최적화되어 있는데, 실수를 계산하려면 정규화 작업을 통해 부동 소수점 방식으로 저장되면서 실수의 연산 속도가 정수 연산보다 느릴 수 밖에 없다.
자, 그런데 이러한 연산을 통해서 우리가 실수를 저장하다 보니까, 만약에 거듭승이 2의 0승이라고 해보자.
그러면 사실은 0이 돼야 되는데, 2의 0승은 1이다. 그러면 사실상 얘는 0.0이라는 값은 절대로 표현할 수 없는 상태가 되는 것이다. 그래서 실수는 사실 정확한 실수를 저장하는 것이 아니라, 그 실수의 가장 근사한 값으로 저장을 하게 되는 것임을 기억해야 한다.
그래서 아주 미세한 오차를 발생을 시킬 수 있는데, 그 미세한 오차를 우리는 일명 부동소수점 오차라고 부르는데, 부동소수점 오차는 사실은 컴퓨터가 내부적으로 실수를 효율적으로 처리함으로써 발생되는 오차이다. 그러니까 실제 정확한 실수값을 저장하는 것이 아니라, 아주 미세하고 아주 근사한 값으로 저장을 하게 되는 거니까, 어떤 공식에 의해서 저장이 되는 거다보니 아주 미세한 오차가 발생을 하게 되는데, 오차도 쌓이게 되면 굉장히 커지게 된다.
그래서 프로그래밍 하는 입장에서는 이러한 오차가 발생할 수 있으니 이 점을 감안하여 코딩을 해주어야 한다.
'C 언어' 카테고리의 다른 글
비트 연산자 (0) | 2024.08.18 |
---|---|
scanf(%d\n, &a)가 틀린 이유 (0) | 2024.08.18 |
C 언어에서 문자열은 문자 배열로 취급된다. (0) | 2024.05.19 |
2차원 배열 동적 할당하기 (0) | 2024.05.15 |
순열 알고리즘: C 프로그래밍에서의 구현 (1) | 2024.05.02 |