스터디/C방장

floating type 사용 시 주의할 점. single/double precision. 메모리 표현 방식. floating type 이야기 [C언어 파헤치기 3]

SW PLAN B 2021. 1. 1. 22:56
728x90

제가 알고 있는 C에 관한 지식을 나눕니다.

카카오톡 오픈 채팅방에서 질문도 받습니다.

궁금한 점들은 댓글 혹은 오픈채팅방에서 질문해 주세요.

C언어 질문하기
C방장 오픈채팅방 QR코드

C방장한테 질문하러 가기


C언어 float single double precision

■ Quiz


int main(void)
{
	float f = 0.0;
	float f_arr[10] = {0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9};
	int i;
	for (i = 0;i < 10;i++)
	{
		if (f == f_arr[i])
		{
			printf("[if] f=%f\n", f);
		}
		else
		{
			printf("[else] f=%f\n", f);
		}
		f = f + 0.1;
	}
	return 0;
}

위 코드의 실행 결과는 어떻게 될까요?

더보기
[if] f=0.000000
[if] f=0.100000
[if] f=0.200000
[if] f=0.300000
[if] f=0.400000
[if] f=0.500000
[if] f=0.600000
[else] f=0.700000
[else] f=0.800000
[else] f=0.900000

f에 0.1을 누적시킨 값과 배열의 값을 비교하고 [if] 또는 [else]와 그 값을 출력하는 코드입니다. 오른쪽에 출력되는 값들은 배열의 값들과 같아 보이는데 왜 0.7부터 [else]가 출력됐을 까요?

지금부터 알아보겠습니다.


■ C언어의 실수(소수점) 표현 방식

표준 C언어는 IEEE 754 부동소수점 표현 방식으로 실수를 표현합니다.

IEEE(Institute of Electrical and Electronics Engineers)는 전기 전자 기술자 협회로 'I-triple-E'(아이 트리플 이)라고 읽습니다. IEEE는 주로 전기 전자 산업 표준을 정하고 알리는 협회입니다. 

C언어는 IEEE 754 부동소수점 표현 방식을 취하기 때문에 여러 시스템에서 호환성과 이식성을 가질 수 있습니다.

부동소수점에서 '부동'은 '움직이지 않는 다'는 뜻의 부동(不動)이 아니라 '고정되어 있지 않고 움직임'을 뜻하는 부동(浮動)입니다. 

type sign bit (부호부, s) exponent bits (지수부, e) mantissa bits (가수부, f_k)
float 1 8 23
double 1 11 52

C언어 float type 표현식
single precision(단 정밀도) 표현 식

single precision(단 정밀도)의 수학적 표현 식은 식1로 나타낼 수 있습니다.

이 식은 부호(sign)를 나타내는 s, 지수부(exponent)를 나타내는 e, 가수부(mantissa)를 나타내는 f로 표현됩니다.

float type 메모리 표현 방법
그림1. single precision(단 정밀도) 메모리 표현

그림1.은 single precision 표현 식을 메모리에 표현한 것을 나타냅니다.

□ 부호부(sign bit)

MSB(Most Significant Bit)는 sign bit[0]을 나타내고 0이면 양수(+) 1이면 음수(-)를 나타냅니다.

□ 지수부(exponent bits)

이어서 exponent bits[1:8]은 2의 지수를 표현합니다. 그런데 이때 지수부는 bias 127을 더한 값이 저장됩니다. 예를 들어 2^0을 표현하려면 exponent bits[1:8]에는 (0+127) 즉, 127이 저장되고 2^-1을 표현하려면 exponent bits[1:8]에는 (-1+127) 즉, 126이 저장됩니다.

e는 -125 이상, 128 이하의 값을 가지므로 exponent bits[1:8] 에는 2 이상 255 이하의 값이 저장됩니다. 이 것을 excess-127 code라고 부릅니다. (참고: double precision(배 정밀도)표현에서 exponent bits[1:12]는 bias 1023 더한 값이 저장되면 excess-1023 code라고 부릅니다.)

□ 가수부(mantissa bits)

C언어 float 식
그림2. floating point의 직관적 표현

가수부는 bit[9]부터 bit[31]까지 각각 이진수의 소수점 첫째 자리부터 소수점 스물세 번째 자리라고 생각하면 쉽습니다.

예를들어 그림2. 의 m이 101(binary) bit[9]=1, bit[10]=0, bit[11]=1, bit[12]=0 ... bit[31]=0이 저장됩니다.

 single precision floating type 예시

□ float type (single precision) memory 해석

C언어 float 메모리 예시
그림3. single precision 메모리 예시

float type의 상수의 메모리에 그림3.의 값이 저장되어있다고 했을 때 이 값을 몇이라고 해석해야 할지 알아보겠습니다.

bit[0] = 0b 이므로 s=0 즉, 이 값은 양수라는 것을 알 수 있습니다.

bit[1:8] = 10000001b 이고 이 값은 십진수로 129입니다. 따라서 e의 값은 129-127, 즉 e=2입니다.

bit[9:31]=0101000...000b 입니다. 따라서 f_1 = 0, f_2=1, f_3=0, f_4=1, f_5=0, f_6=0 .... f_23=0 입니다.

식을 계산하면 x_f = 4 + 4(1/4+1/16) = 5.25라는 결과를 얻을 수 있습니다.

□ 실수를 float type (single precision) memory로 변환

C언어 실수를 float type 변환 방법
그림4. 십진수를 이진수로 변환

실수를 앞에서 배운 메로리 형태로 변환하기 위해선 먼저 이진수로 변환을 해야 합니다.

정수부의 변환은 그림4.에서 왼쪽 박스에서처럼 0이 될 때까지 2를 나누어 줍니다. 그리고 각각에서의 나머지가 이진수의 정수부를 표현합니다. 십진수 5를 이진수로 표현하면 101이 됩니다.

소수부의 변환은 그림4에서 오른쪽 박스에서처럼 1.0이 될 때까지 2를 곱해줍니다. 그리고 각각에서의 일의 자릿수가 소수부를 표현합니다. 십진수 0.25를 이진수로 표현하면 0.01이 됩니다.

만약 소수부가 위 예시처럼 가수부(mantissa bits)로 주어진 메모리 크기(23bit) 내에 표현이 된다면 오차 없이 표현이 가능합니다. 하지만 만약 주어진 가수부(mantissa bits)로 표현이 되지 않는다면 float type으로 표현된 값은 근사값을 가지게 됩니다.

다시 돌아와서 십진수 5.25는 이진수로 101.01로 변환하였습니다. 그리고 이 값을 다시 float type의 메로리 표현에 맞도록 변환해야 합니다.

C언어 실수를 float type 변환 방법2
그림5. float type 표현에 맞도록 변환

이진수 101.01를 그림2. 처럼 변환하면 그림5. 처럼 표현됩니다. 이제 이값을 앞에서 배운 memory에 할당하면 다음과같습니다.

C언어 실수를 float type 변환 방법3
그림6. 이진수의 float type 메모리 저장

이 값은 양수이므로 부호부(sign bit[0])는 0이 됩니다.

excess-127code를 적용하여 지수부(exponent bits[1:8])는 129가 됩니다.

가수부(mantissa bits[9:31])는 각각 0, 1, 0, 1, 0, .... 0이 됩니다.

floating point사용시 주의점

int main(void)
{
	float f = 0.0;
	float f_arr[10] = {0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9};
	int i;
	for (i = 0;i < 10;i++)
	{
		if (f == f_arr[i])
		{
			printf("[if] f=%f, f:0x%x, f_arr:0x%x\n", f, *(int *)&f, *(int *)&f_arr[i]);
		}
		else
		{
			printf("[else] f=%f, f:0x%x, f_arr:0x%x\n", f, *(int*)&f, *(int*)&f_arr[i]);
		}
		f = f + 0.1;
	}
	return 0;
}

Quiz1에서 각 데이터의 메모리에 저장된 값을 hexa로 출력해보겠습니다.

[if] f=0.000000, f:0x0, f_arr:0x0
[if] f=0.100000, f:0x3dcccccd, f_arr:0x3dcccccd
[if] f=0.200000, f:0x3e4ccccd, f_arr:0x3e4ccccd
[if] f=0.300000, f:0x3e99999a, f_arr:0x3e99999a
[if] f=0.400000, f:0x3ecccccd, f_arr:0x3ecccccd
[if] f=0.500000, f:0x3f000000, f_arr:0x3f000000
[if] f=0.600000, f:0x3f19999a, f_arr:0x3f19999a
[else] f=0.700000, f:0x3f333334, f_arr:0x3f333333
[else] f=0.800000, f:0x3f4cccce, f_arr:0x3f4ccccd
[else] f=0.900000, f:0x3f666668, f_arr:0x3f666666

십진수 0.1을 이진수로 표현하면 0.0001100110011...으로 0011이 반복되는 순환 소수로 표현됩니다. 따라서 메모리에 저장되는 값은 정확히 0.1이 아닌 0.1과 가까운 값이 저장됩니다.

두 번째 출력된 f:0x3dcccccd 를 보면 마지막 mantissa bit[31]은 반올림되어 1이 저장된 것을 알 수 있습니다.

반복문을 통해 근사값을 누적시키면 그 오차가 계속해서 누적됩니다. 이것을 오차의 전파라고 합니다.

오차가 전파되며 0.7부터는 메모리에 다른 값이 저장될 정도로 오차가 커지게됩니다.

Quiz1에서 [else]가 출력된 것은 오차가 전파되었기 때문임을 알 수 있습니다.

floating type으로 연산을 할 경우 항상 오차가 있다는 것을 염두에 두고 오차에 따라 프로그램이 오동작 하지 않도록 주의가 필요합니다.


함께 읽으면 좋은 글

2020/12/27 - [스터디/C방장] - 정수형 type 이야기 [C언어 파헤치기 1]

 

정수형 type 이야기 [C언어 파헤치기 1]

제가 알고 있는 C에 관한 지식을 나눕니다. 카카오톡 오픈 채팅방에서 질문도 받습니다. C방장한테 질문하러 가기 ■ QUIZ 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int main(void) {     int a = 0;   ..

sw-daily.tistory.com

2020/12/27 - [스터디/C방장] - 문자형/character type 이야기 [C언어 파헤치기 2]

 

문자형/character type 이야기 [C언어 파헤치기 2]

제가 알고 있는 C에 관한 지식을 나눕니다. 카카오톡 오픈 채팅방에서 질문도 받습니다. C방장한테 질문하러 가기 먼저 읽으면 좋은 글 2020/12/27 - [스터디/C방장] - 정수형 type 이야기 [C언어 파헤

sw-daily.tistory.com

sw-daily.tistory.com/category/스터디/C방장

 

'스터디/C방장' 카테고리의 글 목록

일상의 모든것

sw-daily.tistory.com


반응형