본 포스트팅은 인하대학교 컴퓨터공학과 시스템 프로그래밍 수업자료에 기반하고 있습니다. 개인적인 학습을 위한 정리여서 설명이 다소 미흡할 수 있으니, 간단히 참고하실 분들만 포스팅을 참고해주세요 😎
위처럼 . 을 기준으로 표현.
ex) 1011.101 => 소수점 아래부분은 2^-1 + 2^-3 이 된다.
s : sign bit => 부호 비트. s=0 이면 양수, s=1이면 음수
M : Significand : [1~2) 사이 범위 (1보다 크거나 같고, 2보다 작은 범위) 에 있는 수 (즉, 1.xxxxx 곱하기 2^E 와 같은 형태로 표현하고 싶은 것이다. 왜 2는 안넘어가나면, 2가 넘는 경우는 E의 값을 조정하여 2를 곱해주면 되기 떄문)
E : Exponent => 2의 몇승인지
=> 이 3가지 구성성분이 있다면 모든 소수점에 근사할 수 있다.
Encoding 을 한 경우를 생각해보자.
아래 그림은 각 시스템 bit 체계(32 bit, 64 bit, 80 bit 시스템) 에 따라 s, M, E 를 인코딩해서 저장할때 각각 얼마만큼의 공간을 차지하는지 나타낸 것이다.
Normalized Values 란 앞서 살펴본 공식과 같이 1.xxxx... 에다 2^E 를 곱해준것이다.
E 를 인코딩하는 방법 : E = EXP - Bias
E는 Bitwise 에 쓸 EXP 에다 Bias 라는것을 빼면 계산된다.
Bias = 2^(k-1) -1 => Exp 에 몇 bit를 쓰는가에 따라 결정되는 숫자이다. 예를들어 32비트면 Exp 는 8바이트이므로, 2^(8-1) -1 = 128 - 1= 127이 된다.
아래 예시를 보고 더 이해해보자!
=> Normalized value 로 만드는 과정 : 어떤 숫자가 오던지간에, M이 1로 시작하도록 해당 숫자의 소수점을 맞춰주고, 원래 숫자에 가장 가까울 수 있도록 E 의 값을 결정해서 할당해준다.
1) 위처럼 F = 15213.0 이라는 숫자가 있을 떄 M=1 로 시작하도록 소수점을 맞춰준다. 즉 15213.0 을 2진수로 변환하면 1110...1101 가 되고, 이런 2진수에 대해 소수점을 맞춰주면 1.11011...101 x 2^13 이 된다.
2) 앞서 나온 값 1.110011..1101 x 2^13 에서 1.110011..1101 값이 M 이 되고, 13 이 E가 된다.
3) 이에 따라 frac 은 M의 소수점부분 값인 11011..101 이 된다. 그런데 32bit 시스템이므로(float형은 single precision으로, 32bit 시스템을 가지기때문) frac 이 23bit 의 공간을 차지하므로 11011..101 값으로 채워도 빈 공간이 생기는데, 이 빈공간은 모두 0 으로 채워준다.
4) Bias 값을 계산해보자. 2^(8-1) - 1 = 127이 된다.
5) Exp = E + Bias 이므로, Exp = 13 + 127 = 140 이 된다. 그리고 2진수로 변환하면 10001100 이 된다.
6) 추가적으로 원본 숫자가 양수였으므로, sign bit 은 당연히 0이다.
7) 최종적으로 아래와 같은 결과를 얻을 수 있다.
숫자가 너무 작으면 M 부분이 1로 시작하지 못하고 0.xxxx 형태의 숫자로(즉 0초과 1이하 범위 사이의 숫자) 시작해야 할수도 있다.
=> E 가 쓸수있는 정해진 범위가 있기떄문이다. 표현 가능한 숫자의 범위는 0~255 사이, -128 ~ 127 사이로 범위가 있다. 즉, 2^E 를 통해 -128 밑의 범위는 표현이 불가능하다. 그래서 E의 값으로 가능한 가장 작은 값인 E=0 값보다 더 작아지면 M=1 로 둘 수 없을만큼 작아서 M = 0.xxxx.... 과 같은 형태가 나올 수 있다.
=> 이런 경우를 Denormalized values 라고 한다. 왜냐하면 M=1.xxx 과 같은 형태가 안되기 때문.
시험출제!!! 꼼꼼히 공부하자!
8bit짜리 시스템의 floating point 표현방법을 살펴보자.
sign bit 을 1bit 쓰고, exp 를 4bit 쓰고, frac 을 3bit 쓴다.
bias = 2^(4-1) - 1 = 7이다. 즉 2의 지수를 저장할때 7을 빼서 저장한다.
frac은 3bit 이므로 1.xxx 에서 뒤의 소수점 3개를 저장한다.
아래 표를 참고해서 몇가지 case에 대해 8bit 자리 floating point 임을 가정하고
값(value) 를 계산해보자.
우선 아래의 case 에 대해 계산해보자.
아래 공식을 통해 value 를 계산해보면 M = 1/8 (frac 이 001 이므로) 이고, 2^E = 1/64 이고, s = 0 으로 주어졌으므로
=> value = 1 x 1/8 x 1/64 = 1/512 이 된다.
하나만 더 연습삼아 해보자.
s=0, exp=0110, frac=110 인 경우이다.
bias의 값은 그대로 bias = 7 이고, normalized numbers 임을 가정했으므로
E = exp = bias = 6 - 7 = -1 이다.
그리고 frac = 110 이고, normalized numbers 임을 가정했으므로
M = 1.110 = 3/4 + 4/4 = 7/4 = 14/8 이 된다.
=> 따라서 value = (-1)^s x M x 2^E = 1 x 14/8 x 1/2 = 14/16 이 된다.
이이서 6bit 짜리 시스템에서 floating point 표현했을때에 대해 알아보자.
exp 가 3bit 이고, frac이 2bit 이다.
이에따라 bias = 2^(3-1) - 1 = 3이된다.
실제로 value 값들을 찍어보면 위와 같은 그래프가 나온다.
-15 ~ 15 사이의 범위에 분포해있으며, 0 근처로 갈수록 촘촘해지고 벗어날수록 띄엄띄엄 분포해있다.
=> 이렇듯 6bit 짜리 시스템에서 floating pointer number 로 표현가능한 숫자들은, 0근처의 값들은 촘촘하게 대부분 값들을 표현 가능하나 0에서 벗어날수록 표현 불가능한 숫자들이 많아진다.
floating point number 는
- 1) bit 가 많은 시스템일수록 큰 숫자들에 대해 더 정확하고, 표현 가능한 숫자들이 많아진다. 반면 작은 숫자들에 대해선 부정확하고, 표현 가능한 숫자들이 적어진다.
- 2) 반대로 bit 가 적은 시스템일수록 큰 숫자들에 대해 더 부정확하고, 표현 가능한 숫자들이 적어진다. 반면 작은 숫자들에 대해선 정확하고, 표현 가능한 숫자들이 많아진다.
( 그래서 위에서 보는 6 bit 짜리 시스템에서는 작은 숫자들에 대해서 표현 가능한 것들이 많은 것이다. )
앞선 6 bit 시스템에서 -1~1 사이의 범위를 더 확대해서 자세하게 살펴보자.
floating point number 들은 "근사한" 값들 이므로, 해당 bit 시스템에 맞춰서 가능한 값으로 최대한 Rounding 을 해줘야한다.
- Rounding 이란? : 소수점 . 을 올리거나 내리는 것
=> 소수점을 어떻게 올리고 내리는지 방법이 표준적으로 정의가 되어있다.
두 피연산자 x 와 y 를 더하거나 곱할때, x와 y가 정수라면 그냥 단순히 더하고 곱해주면 된다.
=> f 를 붙여주는 것은 값 y가 타입이 float 이라고 표시한 것이다.
이렇게하면 앞서 배운 floating point number 의 특성때문에, 우리가 생각해 볼 수 있는 x 와 y에 대한 더하기(곱하기) 연산의 결과값과 다르다.
=> 그래서 항상 계산을 실제로 앞서 실제값에 대해 게산을 한 다음에, 계산된 결과에다 시스템의 bit 에 따라서 Round를 적용해줘야한다.
그림을 다시보자. 실제로 x, y의 정확한 값(우리는 계산을 못하는 이상적인 정확한 값) 에 대해서 x+y 연산을 진행하고 그 결과값x+y 에 대해 Rounding 한 결과( = Round(x+y)) 가 나온다.
4가지 rounding mode 가 있다.
우리는 Nearset even 이라는 rounding mode 를 가장 많이 사용한다.
- nearest even : 이진수로 바꿨을때 우리가 저장할 수 있는 bit의 가장 마지막 숫자 LSB 가 짝수여야한다. LSB 가 짝수면 올림되고, 홀수면 버려진다.
- 1) 이진수 숫자로 바꿔놨을 떄 LSB 가 0이면 짝수이다.
- 2) 반올림의 기준값(중간점)은 100...(2)이다.
- CASE1) 중간값을 기준으로 더 작다면 버리고, 더 크다면 올림한다.
- CASE2) 정확히 중간점에 있을때, LSB를 확인해서 기존 숫자가 짝수인지 홀수인지 판단한다. 만일 기존 숫자가 짝수라면, 그리고 올림했을 때와 버림했을 때 결과가 짝수인 경우를 선택해줘서 올림 or 버림해주면 된다.
( => ex. 기존 숫자가 짝수이고 올림 했을때가 짝수, 버렸을때가 홀수라면 올리을 해주는 것이다. )
우리가 소수점 기준으로 2개의 bit 까지만 저장할 수 있다고하자. 그러면 소수점 3번쨰 bit 부터는 round 를 해줘야한다. 즉, 3번쨰 이하의 bit 숫자들에 대을 소수점 2째자리 bit 숫자로 올림을 해줄지 아니면 버릴지를 해줘야한다.
1) 10.00011 에서 011은 중간값(반올림 기준값) 100 보다 작아서 버려진다. 그 결과 10.00이 된다.
2) 10.00110 에서 110은 보다 더 커서 올림해준다. 그 결과 10.01이 된다.
3) 10.11100 에서 100은 정확히 1/2 이다. 올림하냐 버리냐를 판단하기 전에 LSB를 먼저 살펴보면 0이므로 이 숫자는 짝수이다.
올림을 안하고 버린다면 => 결과값이 10.11로, LSB를 확인해보면 홀수가 된다. 따라서 짝수가 홀수로 변하는 것은 안되므로 올림을 해줘야한다.
올림을 진행하면 => 결과값이 11.00 으로 LSB 를 확인해보면 0이므로 짝수가 올림을 진행하면 그대로 짝수가 나오는 것이다. 따라서 올림을 진행해주면 된다.
4) 10.10100 에서 100은 정확히 1/2 이다. 올림 여부를 판단전에 LSB를 확인해보면 0이므로 해당 숫자는 짝수이다. 이 경우는 올림을 하면 LSB가 1이 되버려서, 짝수가 아니게 된다. 따라서 올림하지 않고 그냥 버려줌으로써 짝수가 되게한다. 결과값은 10.10 이다.
반올림(rounding) 의 중간점에 걸치는 경우는, 만일 변환했을 떄 LSB 값에 따라서 짝수가 되는가 아닌가에 따라서 올림 해주냐 마냐가 결정된다.
위와 같은 형태릐 Floating Pointer 숫자 2개를 곱한다.
곱한 결과 sign bit, M, E 의 상태는 아래와 같을것이다.
=> 이렇게 계산하면 M 에 대해 문제점이 발생할 수 있다. M1 X M2 결과가 1~2 사이의 값이여야 하는데, 이 범위를 넘어선 결과값일 수 있다.
E 도 문제가 발생할 수 있다. 2^E1 X 2^E2 = 2^(E1+E2) 결과가 나올텐데, 이 값이 너무커서 표현 가능한 범위를 넘어설 수도 있다.
따라서 이렇게 오버플로우가 발생할 수 있으므로 반드시 shift 연산을 해줘야한다. 이럴 때 좌측or우측 쉬프트를 진행해주는 것을 Post-processing 이라고한다.
아래 예제처럼 M=10.01 인데 1~2 사이의 값이 아니다. 따라서 shift 을 해줘서 10.01 을 1.01 로 만든다. (M=1.xxxxx 형태가 되었다!)
그러고 지수 값에 1을 더해준다. 그러면 2^10 이 2^11 가 되었다.
덧셈은 어떻게할까?
M1 과 M2 의 자릿수가 다르다면 그냥 바로 더하지 못한다.
M은 어떤것은 Normalized, 또 어떤것은 DeNormalized 가 될 수 있다. 그래서 간단하게 M1, M2 를 바로 더하는것이 아니다.
- M1 이 M2보다 더 크다고 가정해보자. 그러면 큰 값인 M1 을 오른쪽으로 밀고, 작은 값 M2 를 왼쪽으로 밀어낸다. 지수값 E1 에서 E2 만큼을 뺀 값(E1 - E2) 만큼을 밀어내면 된다. 그리고 더하면 된다.
=> 쉽게말해, 두 피연산자자의 지수 승 E를 똑같이 맞춰주고 연산해주면 된다.
=> 더한결과 M 값이 도출되고, 이 M 값은 지수값이 E1이 될것이다.
예를들어 두 Floating Point Number 가 둘다 1.1 이라고 하고 덧셈을 한다고 해보자. 그런데, 이 둘을 더한 결과가 2.2가 되지 않는다.
=> 왜냐하면 지수의 승에 따라서 각 피연산자는 예를들어 1.1x2^1, 1.1x2^-1 일수도 있다.
그러면 M1, M2 값은 모두 1.1인데 뒤에 붙은 지수 승 때문에 한 놈이 커지고, 다른 한놈이 작아진다. 따라서 M1 과 M2 를 직접 더하려면 M1 과 M2 사이에서 큰 지수승을 가진 녀석에게 맞춰주는 것이다.
아래 예제를 보자. 둘은 지수승이 2^1, 2^-1 으로 다르다. 2번째 피연산자를 E1-E2 만큼 밀어줘서 M1 과 M2를 계산하기 적절하게 자릿수를 조절해주면 바로 계산이된다.
앞선 과정으로 M1 과 M2 에 대해 바로 계산이 되도록 셋팅되었다면,
곱셈 연산과 동일하게 Post-processing 을 진행해준다.
실제 값을 Floating Point 에 저장해서 Floating point 숫자로 만드는 방법을 알아보자. 아래 예시에서는 실제 값을 소수가 아닌 정수값에 대해 Floating point 를 적용해보는 것을 살펴보겠다.
즉 아래처럼, M 값을 변형시켜줌에 따른 frac 을 만들고 (이떄 새롭게 채워지는 bit는 전부 0으로 채워줌)
E 값도 생성해준다.
그런데 frac 부분에는 bit 를 3개까지 밖에 저장을 못하므로, rounding 을 해줘야한다. rounding 을 해서 소수점 3째자리에 대해 반올림을 해준다.
앞선 rounding 을 통해 오버플로우가 발생한 값에 대해 진행해주면된다.
c언어에서 Floating point 가 어떻게 구현되어 있는지를 알아보자.
casting(형변환) 과정을 하는것을 보자.
c언어에서 다양한 타입간의 형변환