형변환 및 연산에 대하여

webper81의 이미지

안녕하세요. 지금 간단한 시간을 계산하는 프로그래밍을 작성중입니다.

  temp1=(float)mktime(&now);
	temp2=atof(s_HH)*3600+atof(s_MM)*60+atof(s_SS)+atof(s_MS)/100;
	totalSum=temp1+temp2;
	printf("temp1 %f temp2 %f, sum %f totoalSum %f\n", temp1, temp2, temp1+temp2, totalSum);

이렇게 해서 결과 값을 찍어보면 다음과 같습니다.

temp1 1064937600.000000 temp2 1.000000, sum 1064937601.000000 totoalSum 1064937600.000000

즉, temp1+temp2해서 값이 더해지는곳에서 다른 변수를 사용해서

더하면 정확이 더해지지 않습니다. 왜이런 걸까요? 형변환이 잘못된 건가요?

여러분들의 답변 부탁드립니다.

bugiii의 이미지

totalSum 변수가 float 로 선언되어 있나요?
그렇다면 double 로 바꾼 다음 결과를 알려주세요.

webper81의 이미지

double을 사용하니 되는군요. 이유를 아시나요? 궁금해서리...

winner의 이미지

float 은 대게 십진수 6자리의 유효숫자한계를 갖습니다.

bugiii의 이미지

컴파일러가 gcc 라면 -O1 정도로 다시 컴파일하시고 결과를 보여주세요.
또 틀린 결과가 나올 것입니다.

그리고,

<unsigned> [char, short] x, y;
int z;
x = 각 타입의 절반을 넘는 임의의 숫자;
y = 마찬가지;
z = x + y;

이렇게도 한번씩 실험해 보세요.

eungkyu의 이미지

유효숫자의 개수가 float, 또는 double의 한계를 넘어서 그렇습니다.
실수를 저장하거나 계산하는 내부 방식을 조금만 이해하신다면 쉽게 알 수 있을 것입니다.
괜히 실수와 정수가 따로있는게 아니고, 괜히 같은 크기의 메모리에 저장할 수 있는 실수의 범위가 큰게 아니죠 :)

정확한 계산을 원하신다면, long long 형을 사용해야 할 거 같네요. (물론 다루는 수의 범위가 long long 의 범위 안에 든다면)

bugiii의 이미지

대입 연산자의 왼쪽에 있는 변수의 형은 그 오른쪽의 연산과정과 무관해야 한다고 생각합니다. 그렇다면 아래와 같은 표현은 (엄밀히 따지자면) float 연산의 결과인 float 범위내의 결과값이 되어야 할 것입니다.

float a = 1064937600.0f, b = 1.0f;
double c;
c = a + b; // LINE_A
printf( "%f %f", a + b, c ); // LINE_B

LINE_A 에서의 연산 결과값은 (연산만을 놓고 봤을 때) float 결과값이어야 하므로 1064937600.0f 이어야 하는데, 이상하게도 제대로 된 double 값 1064937601.0 이 들어갑니다. LINE_B 도 마찬가지입니다.

정확하게 하자면 LINE_A 에서 a 나 b 최소 하나에 double 로 캐스팅을 하지 않는 이상 a + b 수식의 결과는 float 범위내에서의 연산결과이어야 함에도 불구하고 그 결과값이 double 연산의 결과값이 되는 경우도 있다는 것입니다.

아까전에 말씀드린 -O1 옵션은 어셈블리로 뽑아보니 최적화로 상수 상태에서의 float 연산을 컴파일시 직접 수행해서 나온 결과이므로 무효이고, 덧셈 연산을 따로 함수로 만들었더니 위와 똑같은 결과가 나왔습니다.

참고로, printf 의 가변인자로 넘어가는 실수는 아마도 double 형으로 변환되어 넘어간다고 추정할 수 있습니다. (그럴 것입니다.) 하지만 이것도 위와 똑같은 경우라고 생각합니다.

이러한 현상은 char, short, int 에서도 발생합니다.

어셈블리로 뽑았을 때 추정해 볼 수 있는 것은 연산의 내부는 실수연산 레지스터를 이용하는데 이것이 64비트 크기이고 double 에 맞다보니 그 연산의 결과를 다시 float 로 맞추지 않고 그냥 double 변수에 저장시킨 것 같습니다.

연산의 결과가 어떠해야 한다는 것이 (연산에서 자동 형변환과는 다른) C 표준안에 어떻게 정의되어 있는지는 모르겠습니다만, 조금 요상한 결과가 나올 수도 있다고 생각합니다. (의도하지 않은 결과) 잘 아시는 분들의 의견을 듣고 싶습니다.

그럼, 이만...

BL의 이미지

http://www.gpgstudy.com/forum/viewtopic.php?t=1603&highlight=float+%C7%D1%B0%E8

VC++에 관한 예기지만 원인은 같을것이라고 생각합니다.

lsj0713의 이미지

bugiii wrote:

참고로, printf 의 가변인자로 넘어가는 실수는 아마도 double 형으로 변환되어 넘어간다고 추정할 수 있습니다. (그럴 것입니다.) 하지만 이것도 위와 똑같은 경우라고 생각합니다.

네 맞습니다. printf 함수 뿐만 아니라 모든 가변 인자 함수는 매개변수를 넘길 때 기본 인자 진급(default argument promotion) 과정을 거칩니다. 이 과정에서 float형은 double 형으로, char, unsigned char, signed char, short, unsigned short형은 int형 또는 unsigned int형으로, 모든 포인터 형은 void* 형으로 변환되어 전달됩니다. 이것은 각 구현체에서 가장 효율적인 방법으로 가변 인자 함수를 구현할 수 있도록 하는 C언어의 배려 입니다.

아마도 위의 결과는 수를 여러가지로 바꿔가면서 시험해보면 여러가지 다른 결과가 나올 꺼라 예상됩니다.

전웅의 이미지

bugiii wrote:
그리고,

<unsigned> [char, short] x, y;
int z;
x = 각 타입의 절반을 넘는 임의의 숫자;
y = 마찬가지;
z = x + y;

이렇게도 한번씩 실험해 보세요.

보여주신 예의 의도는 이해합니다만, 현재 문제가 되는 부분과는 다소
거리가 있습니다. 덧셈 연산자는 정수 피연산자에 대해 integral promotion
을 일으키게 되고, x, y 는 각각 unsigned int 나 int 로 변환되어 연산이
수행됩니다. 따라서, x + y 의 결과 역시 진급된 피연산자와 동일한 type
을 갖습니다. 하지만,

bugiii wrote:
대입 연산자의 왼쪽에 있는 변수의 형은 그 오른쪽의 연산과정과 무관해야 한다고 생각합니다. 그렇다면 아래와 같은 표현은 (엄밀히 따지자면) float 연산의 결과인 float 범위내의 결과값이 되어야 할 것입니다.

float a = 1064937600.0f, b = 1.0f;
double c;
c = a + b; // LINE_A
printf( "%f %f", a + b, c ); // LINE_B

LINE_A 에서의 연산 결과값은 (연산만을 놓고 봤을 때) float 결과값이어야 하므로 1064937600.0f 이어야 하는데, 이상하게도 제대로 된 double 값 1064937601.0 이 들어갑니다. LINE_B 도 마찬가지입니다.

정확하게 하자면 LINE_A 에서 a 나 b 최소 하나에 double 로 캐스팅을 하지 않는 이상 a + b 수식의 결과는 float 범위내에서의 연산결과이어야 함에도 불구하고 그 결과값이 double 연산의 결과값이 되는 경우도 있다는 것입니다.

아까전에 말씀드린 -O1 옵션은 어셈블리로 뽑아보니 최적화로 상수 상태에서의 float 연산을 컴파일시 직접 수행해서 나온 결과이므로 무효이고, 덧셈 연산을 따로 함수로 만들었더니 위와 똑같은 결과가 나왔습니다.

참고로, printf 의 가변인자로 넘어가는 실수는 아마도 double 형으로 변환되어 넘어간다고 추정할 수 있습니다. (그럴 것입니다.) 하지만 이것도 위와 똑같은 경우라고 생각합니다.

이러한 현상은 char, short, int 에서도 발생합니다.

어셈블리로 뽑았을 때 추정해 볼 수 있는 것은 연산의 내부는 실수연산 레지스터를 이용하는데 이것이 64비트 크기이고 double 에 맞다보니 그 연산의 결과를 다시 float 로 맞추지 않고 그냥 double 변수에 저장시킨 것 같습니다.

연산의 결과가 어떠해야 한다는 것이 (연산에서 자동 형변환과는 다른) C 표준안에 어떻게 정의되어 있는지는 모르겠습니다만, 조금 요상한 결과가 나올 수도 있다고 생각합니다. (의도하지 않은 결과) 잘 아시는 분들의 의견을 듣고 싶습니다.

(정의에 의해) 정확한 결과를 예측 가능한 정수 연산과는 달리 부동형과
관련된 연산은 연산이나 피연산자가 명시하는 type 보다 넉넉한 precision
으로 연산될 수 있습니다 - 이는 표준에 의해 의도적으로 허락된 것입니다.
즉, 보여주신 예에서 분명히 볼 수 있듯이, 덧셈 연산자의 두 피연산자가
float 형인 경우 연산의 type 자체 (즉, type algebra) 는 float 형으로
수행되지만 실제 내부 연산은 flaot 이상의 precision 을 갖는 type
(double 이나 long double) 으로 이루어질 수 있습니다. 이는 부동 소수
연산에 대해 사실상 extended double precision 만을 제대로 (fully)
지원하는 extended-based system (Intel 계열이 여기에 속합니다 - 반대로
Motorola 68000 시리즈는 extended double precision 을 지원하지 않고
single 과 double precision 을 제대로 지원하여 대조를 이룹니다) 를
배려하기 위한 것으로, 동일한 이유로 IEEE 754 표준 역시 연산의 결과가
round 되는 precision 인 destination 의 개념을 별도로 도입하고 있습니다.

결과적으로, 부동 소수 연산이 실제 이루어지는 내부 과정은 (별도로
세세한 옵션으로 제어해 주지 않는 이상 - 물론, 항상 그런 옵션이 제공
된다는 보장은 없습니다) 컴파일러를 포함한 implementation 이 임의로
결정한다고 볼 수 있는 것이며 (이는 그렇게 하는 것이 더 효율적이라면
implementation 이 항상 더 넉넉한 precision 을 제공하여 더 빠르고 더
정밀한 결과를 보장해 줄 수 있다는 항상 참은 아닌 믿음에서 비롯됩니다),
이와 같은 특성은 일반적인 경우에는 더 나은 성능을 보여주지만 매우
특수한 (하지만 단순한 목적의) 알고리즘에서 double-rounding 으로 인한
큰 오차를 만들어 내는 원인이 됩니다.

참고로, C99 에서는 실제 부동 소수 연산이 이루어지는 type 을 프로그램
내에서 확인 혹은 부분적으로 제어할 수 있도록 (아직은 부족하지만)
다양한 장치를 지원하고 있으며, (제 기억이 맞다면) 현재 ANSI 에서 표준
개정중인 포트란 역시 유사한 (더 나은?) 지원을 해줄 것으로 생각합니다.

물론, C90/C99 에서도 대입이나 캐스트에 의한 명시적 변환의 경우에는
(double-rounding 문제는 본질적으로 피할 수 없을지라도) 겉으로 보이는
type 이 명시하는 precision 으로 값을 변환하기를 요구하기 때문에,
다수의 경우에 임시 변수로의 대입이나 캐스트 연산을 통해 어느 정도까지
는 원하는 결과를 얻을 수 있습니다. 예를 들면,

#include <stdio.h>

float add(a, b)    /* for suppressing constant folding and propagation */
{
    return a + b;    /* no conversion required! */
}

int main(void)
{
    float a = 1064937600.0f, b = 1.0f;
    double c;
    float d;

    c = (float)(a + b);    /* conversion required, but failed in many implementations */
    d = a + b;             /* conversion required */
    printf("%f %f", c, d);
    printf( " %f\n", (float)(a + b));    /* conversion required, but failed in ... */

    return 0;
}

"conversion required" 라고 주석을 붙인 부분에서는 실제 float 형으로의
값 변환이 요구됩니다 - 하지만, 같은 type 으로의 캐스트 연산의 경우
제가 가지고 있는 gcc 를 포함해 다수의 컴파일러에서 이를 제대로 수행
하지 못합니다, 물론 이는 분명 표준을 따르지 않는 컴파일러의 버그에
해당합니다.

lsj0713 wrote:
printf 함수 뿐만 아니라 모든 가변 인자 함수는 매개변수를 넘길 때 기본 인자 진급(default argument promotion) 과정을 거칩니다.

가변 인자에 대해서만 기본 인자 진급이 수행됩니다. 고정 인자 부분에는
함수 원형에 의한 변환만이 있을 뿐입니다.

lsj0713 wrote:
이 과정에서 ... 모든 포인터 형은 void* 형으로 변환되어 전달됩니다.

다시 한번 확인해 보시기 바랍니다. 기본 인자 진급은 포인터형에는
적용되지 않습니다. 따라서, 다음 코드는 잘못된 것입니다.

char c, *p = &c;
printf("%p\n", p);    /* wrong; cast to void * required */

그럼...

--
Jun, Woong (woong at gmail.com)
http://www.woong.org

bugiii의 이미지

전웅님 말씀 감사합니다. 좋은 것 또 배우고 갑니다.

lsj0713의 이미지

전웅 wrote:

lsj0713 wrote:
이 과정에서 ... 모든 포인터 형은 void* 형으로 변환되어 전달됩니다.

다시 한번 확인해 보시기 바랍니다. 기본 인자 진급은 포인터형에는
적용되지 않습니다. 따라서, 다음 코드는 잘못된 것입니다.

char c, *p = &c;
printf("%p\n", p);    /* wrong; cast to void * required */

그럼...

네 맞습니다. 오독으로 인한 저의 실수였습니다. 기본 인자 진급에 포인터의 변환은 포함되지 않습니다. 지금까지 잘못 알고 있었습니다. 정식으로 읽지를 않고 필요할때마다 관련 부분만 찾아서 읽다 보니 이런 실수가 빈번히 일어나는군요-_-;;

그러나, 다음 두 내용의 연결에 따라서 위의 코드는 적법한 코드가 되는 것이 아닌지요? 얼핏 생각해봐도, char*형과 void*형은 같은 내부 표현을 가지므로 별 문제가 없을 것 같기도 합니다만... 아 물론 모든 포인터형에 대해서 얘기하는 것이 아니라 char*형과 void*형에 대해서만 말하는 것입니다.

Quote:

C99 6.5.2.2p6

...If the function is defined with a type that does not include a prototype, and the types of the arguments after promotion are not compatible with those of the parameters after promotion, the behavior is undefined, except for the following cases:

- one promoted type is a signed integer type, the other promoted type is the corresponding unsigned integer type, and the value is representable in both types;
- both types are pointers to qualified or unqualified versions of a character type or void.

C99 7.1.5.1.1p2

If there is no actual next argument, or if type is not compatible with the type of the actual next argument (as promoted according to the default argument promotions), the behavior is undefined, except for the following cases:

- one type is a signed integer type, the other type is the corresponding unsigned integer type, and the value is representable in both types;
- one type is pointer to void and the other is a pointer to a character type.

6.5.2.2p6에서 인용한 내용이 원형이 없이 정의된 함수에만 적용되느냐 아니면 가변 인자 함수에도 적용되느냐 하는 것이 좀 아리송 하군요. 일단은 '원형이 없이 정의된 함수라면'이라고 쓰여 있긴 하지만 기본 인자 진급에 대한 얘기를 설명하는 문단에서 나왔으므로 가변 인자 함수에 적용될 것 같기도 하고...

전웅의 이미지

lsj0713 wrote:
네 맞습니다. 오독으로 인한 저의 실수였습니다. 기본 인자 진급에 포인터의 변환은 포함되지 않습니다. 지금까지 잘못 알고 있었습니다. 정식으로 읽지를 않고 필요할때마다 관련 부분만 찾아서 읽다 보니 이런 실수가 빈번히 일어나는군요-_-;;

그러나, 다음 두 내용의 연결에 따라서 위의 코드는 적법한 코드가 되는 것이 아닌지요? 얼핏 생각해봐도, char*형과 void*형은 같은 내부 표현을 가지므로 별 문제가 없을 것 같기도 합니다만... 아 물론 모든 포인터형에 대해서 얘기하는 것이 아니라 char*형과 void*형에 대해서만 말하는 것입니다.

Quote:

C99 6.5.2.2p6

...If the function is defined with a type that does not include a prototype, and the types of the arguments after promotion are not compatible with those of the parameters after promotion, the behavior is undefined, except for the following cases:

- one promoted type is a signed integer type, the other promoted type is the corresponding unsigned integer type, and the value is representable in both types;
- both types are pointers to qualified or unqualified versions of a character type or void.

C99 7.1.5.1.1p2

If there is no actual next argument, or if type is not compatible with the type of the actual next argument (as promoted according to the default argument promotions), the behavior is undefined, except for the following cases:

- one type is a signed integer type, the other type is the corresponding unsigned integer type, and the value is representable in both types;
- one type is pointer to void and the other is a pointer to a character type.

6.5.2.2p6에서 인용한 내용이 원형이 없이 정의된 함수에만 적용되느냐 아니면 가변 인자 함수에도 적용되느냐 하는 것이 좀 아리송 하군요. 일단은 '원형이 없이 정의된 함수라면'이라고 쓰여 있긴 하지만 기본 인자 진급에 대한 얘기를 설명하는 문단에서 나왔으므로 가변 인자 함수에 적용될 것 같기도 하고...

표준이 prose 형식으로 작성되어 있다보니 읽는 순서나 겹치는 내용의 우선 순위에 의해 어느 정도 다른 해석이 가능한 것이 사실입니다. 모든 경우에 적용할 수 있는지를 검사해 보지는 않았지만, 이와 같은 경우에 일반적으로 정의가 항상 우선하는 것으로, 상세한 규칙이 보다 일반적인 규칙에 우선하는 것으로 이해하면 거의 올바른 해석을 이끌어 낼 수 있습니다. 간단히 이야기해 printf() 함수의 행동은 위에 언급하신 내용 보다 printf() 의 spec 자체가 우선적으로 적용됩니다.

Quote:
p The argument shall be a pointer to void. The value of the pointer is
converted to a sequence of printing characters, in an implementation-defined
manner.

따라서, printf(%p) 에는 void * 만을 전달해야 합니다. 물론, 이는 다른 부분 (언급하신 부분) 에서 char * 와 void * 의 type punning 을 허락하는 것과는 다소 대조적인 모습입니다 - char *, void * 사이의 혹은 공통 범위 내의 다른 무유부호 정수형 사이의 type punning 을 허락하는 부분은 사실 C99 에서 새로 추가된 부분입니다. 즉, 현실적으로 printf(%p) 에 char * 를 전달하는 것이 문제를 일으킬 수 있는 경우는 implementation 이 printf() 의 format string 과 인자의 type 을 비교해 검사하거나, 인자와 함께 내부적으로 인자의 데이터형이 전달되어 printf() 내에서 이를 검사하는 경우 뿐입니다.

굳이 printf(%p) 에 대해서 동일한 표현인 char * 를 허락하지 않는 구체적인 근거는 모르겠습니다만 (printf() 의 format string 과 인자를 비교 검사하는 기존의 implementation 에게는 char * 를 배제하는 것이 작은 이익이 될 수는 있습니다만), printf() 에 있는 보다 더 기괴한 부분을 생각했을 때 납득이 되지 않는 것은 아닙니다.

그럼...

--
Jun, Woong (woong at gmail.com)
http://www.woong.org

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.