C프로그래밍 공부를 하다, 컴퓨터가 실수를 나타내는 방식으로 부동 소수점을 배웠는데 구조가 어려워 정리하는 목적으로 작성하려 한다.
컴퓨터가 실수를 나타내는 방식에는 고정 소수점과 부동 소수점이 있다.
고정 소수점은 실수를 정수부(정수 부분)와 소수부(소수점 뒷 부분)으로 나눈다. 이때 소수부의 자릿수를 미리 정하고, 고정된 자릿수의 소수를 표현한다. 하지만 이는 자릿수를 고정하기에 표현할 수 있는 범위가 적어 잘 사용되지 않는다.
부동 소수점은 실수를 부호부와 지수부(Exponent), 가수부(Mantissa)로 나누어 표현하는 방식으로 정규식을 사용한다.
전 강의에서 배웠듯 Float는 4byte, Double은 8byte의 크기를 가진다. 1byte는 8bits이기에 Float는 총 32bits를 가지고 Double은 64bits를 가진다. 이로 인해 Double이 표현할 수 있는 수의 범위가 더 커지는 것이다.
정규형에선 정수를 한 자리수(1)만 남김으로써 표현법을 통일하려 한다. 정규형을 사용하지 않으면 하나의 숫자에 대해 너무나 많은 표현이 가능하기 때문이다. 먼저 밑의 IEEE 754 표준을 보자.
IEEE 754 표준방식에 따르면, 정규형은 다음과 같다.
하나씩 나누어 설명하자면 다음과 같다.
Double과 Float 모두 1bit는 sign bit(=MSB(Most Significant Bit))에 할당한다. 이를 통해 우리는 이 실수의 부호를 확인할 수 있다.
지수 부분의 S는 sign bit를 의미한다. sign bit가 1이면 (-1)이 되어 음수가 되고, 0이면 (1)이 되어 양수가 된다.
M은 Mantissa, 즉 가수부를 나타낸다. 가수부에는 소수점 뒷자리를 표기하기 위한 비트들이 들어간다. 여기서의 M은 십진법으로 표현하지만, 나중에는 2진수로 변환하여 표기하여야 한다. 이해가 가지않는다면 가장 밑의 예시를 보자.
정규형에서 앞의 정수 부분이 왜 1인지 의문이 생긴다. 이는 가수부의 첫 번째 비트를 항상 1로 가정한다는 뜻인데, 다음과 같은 질문에 우리 교수님은 다음과 같은 답을 해주셨다.
"every value except the 0 starts with 1. by defining like this, 1 bit can be saved."
쉽게 말해, 가수부는 23 bit로 구성되어 있다는 것을 위의 도식을 통해 알 수 있는데, 정규화를 하게 되면 가수부의 정수부는 항상 1이 된다.
이때, "당연히 1인 것을 알고 있기 때문에" 가장 앞의 1(1 bit)을 실제 메모리에 저장하지 않고 존재한다고 가정한다. 따라서 실제로는 24bit를 사용해 가수부를 표현할 수 있으므로, 비트 하나를 아껴 소수점 자리를 더욱 정확히 표현할 수 있게 되는 것이다.
이를 "hidden-bit"를 사용한다고 말한다.
E는 Exponent, 즉 지수부를 나타낸다. 지수부는 소수점의 위치를 나타낸다.
* 만약 지수부가 3이라면, 소수점 3번째 자릿수부터 값이 있다는 것을 의미한다.
이 표현식에서, E는 아직 10진수이다. 그러나 결국 E는 Binary number로 변환되어야 하고, 그 Binary number는 위의 8 bit(Float) 혹은 11 bit(Double)안에 들어간다.
* 이해가 가지 않는다면 가장 밑의 예시를 보자!
IEEE 754 표준에서 사용하는 32비트 부동 소수점 수(Float Type)에서 지수 부분은 8비트로 구성되어 있으며, excess-127 표기법을 사용한다.
지수를 나타내는 비트 패턴에 127을 더한 값을 실제 지수 값으로 사용한다.
그렇다면 왜 127이라는 숫자를 쓰는 것일까?
위의 식에서 Exponent에 8 bits를 할당하므로 256(2^8)까지를 나타낼 수 있다.
이때 일반적으로 사용되는 범위인 0~255를 사용하지 않는다. 0은 0을 표현하기 위해, 255는 무한을 표한하기 위해 예약되어 있기 때문이다. (원문 : 0 and 255 are reserved for special use.) 따라서 1~254가 사용된다.
이때 우리는 1~254에서 절반을 음수에, 나머지 절반을 양수에 할당한다. 이때 정규화하기 위해 127을 E에서 빼주게 된다.
EXCESS-127 표기법은 교수님이 설명한 바 없습니다. E-127을 이해하기 위해 더 찾아본 것을 기입한 것임을 밝힙니다.
두 부동소수점 수를 비교할 때 각각의 지수를 비교하는 것이 아니라, 비트 패턴을 비교하여 부동소수점 수의 크기를 판단한다. 이때 지수값에 127을 더한 비트 패턴을 사용하면, 지수값이 같은 두 부동소수점 수를 비교할 때 각각의 비트 패턴을 비교 연산자로 비교하여 크기를 판단할 수 있다.
예를 들어, 두 개의 부동소수점 수 A와 B가 있고, 이들의 부호, 지수 및 가수를 excess-127 표기법으로 나타내면 다음과 같다고 가정해보자.
A = 0 10000010 10100000000000000000000
B = 0 10000011 01000000000000000000000
이제 이 두 수를 비교해보자.
이해가 잘 가지 않는다면, 다음과 같은 질답을 확인해보자.
Question:
Why do we add 127 to the exponent in IEEE-754 floating number format to get the actual exponent value?
Answer:
The exponent in IEEE 754 single precision is in Excess-127 format, which means you subtract 127 from the exponent field to get the number’s actual exponent. You add 127 to the exponent value to get the value to put into the exponent field.
This ends up giving IEEE 754 some interesting properties:
All bits zero naturally means zero. That’s both convenient and conceptually nice.
The encodings for subnormal numbers flow smoothly into the normal numbers.
If you have two floating point numbers, you can determine which one has the larger magnitude by interpreting the lower 31 bits as an integer and performing an integer comparison.
That last property is perhaps the most mind blowing when you first notice it. Basically, there is a natural ordering among all floating point values, and if you ignore the sign bit, it exactly corresponds to the ordering of integers expressed in the same number of bits.
This property makes it really easy to generate a total ordering primitive that’s no more expensive than an integer comparison. It also makes it easy to perform error calculations in terms of ULPs (units of the last place) by simply performing an integer subtraction on the bit patterns for the numbers.
The all-bits-zero-means-0.0 aspect is handy. Many languages specify a default initializer of 0.0 for floating point variables. (In C and C++, it only applies to globals; others may apply it to locals as well.) Zeroing memory is often cheaper than initializing it to other values.
If the exponent field were instead expressed as a signed exponent, the value 0.0 would have a more awkward representation, probably 4000000016
and all-bits-zero would likely correspond to 1.0 or 2.0. That just feels a bit weird, and in the face of default initializers, far less convenient.
예시를 보자. 다음과 같은 숫자가 컴퓨터에 Float type(single precision)으로 어떻게 저장될까?
-0.15625(decimal)
-0.00101
1 01111100 01000000000000000000000
(Sign Bit) (Exponent) (Fraction)