제가 알고 있는 C에 관한 지식을 나눕니다.
카카오톡 오픈 채팅방에서 질문도 받습니다.
궁금한 점들은 댓글 혹은 오픈채팅방에서 질문해 주세요.
■ 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 |
single precision(단 정밀도)의 수학적 표현 식은 식1로 나타낼 수 있습니다.
이 식은 부호(sign)를 나타내는 s, 지수부(exponent)를 나타내는 e, 가수부(mantissa)를 나타내는 f로 표현됩니다.
그림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)
가수부는 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 해석
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로 변환
실수를 앞에서 배운 메로리 형태로 변환하기 위해선 먼저 이진수로 변환을 해야 합니다.
정수부의 변환은 그림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의 메로리 표현에 맞도록 변환해야 합니다.
이진수 101.01를 그림2. 처럼 변환하면 그림5. 처럼 표현됩니다. 이제 이값을 앞에서 배운 memory에 할당하면 다음과같습니다.
이 값은 양수이므로 부호부(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]
2020/12/27 - [스터디/C방장] - 문자형/character type 이야기 [C언어 파헤치기 2]
sw-daily.tistory.com/category/스터디/C방장
'스터디 > C방장' 카테고리의 다른 글
1. 컴퓨터 구조의 간단한 이해 [C언어 포인터 어려워도 제대로 이해하기] (0) | 2021.01.17 |
---|---|
변수의 scope rule, lifetime / extern, static의 올바른 사용 방법 (0) | 2021.01.07 |
협업을 위한 코드, 읽기 쉬운 코드 작성하는 법. Top Down, 모듈화 (feat. 소수 구하기) (0) | 2021.01.03 |
문자형/character type 이야기 [C언어 파헤치기 2] (0) | 2020.12.27 |
정수형 type 이야기 [C언어 파헤치기 1] (0) | 2020.12.27 |