다음 코드를 컴파일합니다.
double getDouble()
{
double value = 2147483649.0;
return value;
}
int main()
{
printf("INT_MAX: %u\n", INT_MAX);
printf("UINT_MAX: %u\n", UINT_MAX);
printf("Double value: %f\n", getDouble());
printf("Direct cast value: %u\n", (unsigned int) getDouble());
double d = getDouble();
printf("Indirect cast value: %u\n", (unsigned int) d);
return 0;
}
출력 (MSVC x86) :
INT_MAX: 2147483647
UINT_MAX: 4294967295
Double value: 2147483649.000000
Direct cast value: 2147483648
Indirect cast value: 2147483649
출력 (MSVC x64) :
INT_MAX: 2147483647
UINT_MAX: 4294967295
Double value: 2147483649.000000
Direct cast value: 2147483649
Indirect cast value: 2147483649
에서 Microsoft 설명서 에서 전환으로 최대 값의 정수 서명에 대한 언급이없는 double
하려면 unsigned int
.
위의 모든 값 은 함수가 반환 될 때 INT_MAX
잘립니다 2147483648
.
내가 사용하고 비주얼 스튜디오 2019 프로그램을 구축 할 수 있습니다. 이것은 gcc 에서는 발생하지 않습니다 .
내가 뭔가 잘못하고 있습니까? 로 변환하는 안전한 방법 double
이 unsigned int
있습니까?
답변
컴파일러 버그 …
@anastaciu에서 제공하는 어셈블리에서 직접 캐스트 코드는을 호출하는데 __ftol2_sse
, 이는 숫자를 부호있는 long 으로 변환하는 것 같습니다 . 루틴 이름은 ftol2_sse
이것이 sse 사용 가능 기계이기 때문입니다. 그러나 float는 x87 부동 소수점 레지스터에 있습니다.
; Line 17
call _getDouble
call __ftol2_sse
push eax
push OFFSET ??_C@_0BH@GDLBDFEH@Direct?5cast?5value?3?5?$CFu?6@
call _printf
add esp, 8
반면 간접 캐스트는
; Line 18
call _getDouble
fstp QWORD PTR _d$[ebp]
; Line 19
movsd xmm0, QWORD PTR _d$[ebp]
call __dtoui3
push eax
push OFFSET ??_C@_0BJ@HCKMOBHF@Indirect?5cast?5value?3?5?$CFu?6@
call _printf
add esp, 8
이중 값을 팝하고 로컬 변수에 저장 한 다음 SSE 레지스터에로드하고 __dtoui3
부호없는 int 변환 루틴에 대한 double을 호출합니다 .
직접 캐스트의 동작이 C89를 따르지 않습니다. 또한 이후 개정판을 따르지 않습니다. C89 조차도 다음과 같이 명시 적으로 말합니다.
정수 유형의 값이 부호없는 유형으로 변환 될 때 수행되는 나머지 연산은 부동 유형의 값이 부호없는 유형으로 변환 될 때 수행 될 필요가 없습니다. 따라서 이식 가능한 값의 범위는 [0, Utype_MAX + 1) 입니다.
나는 문제가 2005 년부터 계속 될 수 있다고 생각한다. 예전 에는 __ftol2
이 코드에서 작동했을 라는 변환 함수 가 있었다. 즉, 값을 부호있는 숫자 -2147483647 로 변환 하여 올바른 값을 생성했을 것이다. 부호없는 숫자로 해석 될 때의 결과.
안타깝게도을 (를 __ftol2_sse
) 드롭 인 대체 __ftol2
할 수 없습니다.-그저 최하위 값 비트를 그대로 사용하는 대신 LONG_MIN
/ 를 반환하여 범위를 벗어난 오류 0x80000000
를 알리기 때문에 여기에서는 부호없는 길이로 해석되지 않습니다. 기대했던 모든 것. double a value> to의 변환 은 정의되지 않은 동작 을 가지므로 의 동작 __ftol2_sse
은에 대해 유효합니다 .signed long
LONG_MAX
signed long
답변
@AnttiHaapala의 답변에 따라 최적화를 사용하여 코드를 테스트 한 /Ox
결과 __ftol2_sse
더 이상 사용되지 않는 버그가 제거된다는 것을 알았습니다 .
//; 17 : printf("Direct cast value: %u\n", (unsigned int)getDouble());
push -2147483647 //; 80000001H
push OFFSET $SG10116
call _printf
//; 18 : double d = getDouble();
//; 19 : printf("Indirect cast value: %u\n", (unsigned int)d);
push -2147483647 //; 80000001H
push OFFSET $SG10117
call _printf
add esp, 28 //; 0000001cH
최적화는 인라인 getdouble()
되고 상수 표현식 평가를 추가하여 런타임에 변환 할 필요가 없으므로 버그가 사라집니다.
호기심으로 인해 런타임에 float-int 변환을 강제하도록 코드를 변경하는 등 몇 가지 테스트를 수행했습니다. 이 경우 결과는 여전히 정확하며 컴파일러는 최적화 __dtoui3
를 통해 두 변환 모두 에서 사용합니다 .
//; 19 : printf("Direct cast value: %u\n", (unsigned int)getDouble(d));
movsd xmm0, QWORD PTR _d$[esp+24]
add esp, 12 //; 0000000cH
call __dtoui3
push eax
push OFFSET $SG9261
call _printf
//; 20 : double db = getDouble(d);
//; 21 : printf("Indirect cast value: %u\n", (unsigned int)db);
movsd xmm0, QWORD PTR _d$[esp+20]
add esp, 8
call __dtoui3
push eax
push OFFSET $SG9262
call _printf
그러나 인라인을 방지 __declspec(noinline) double getDouble(){...}
하면 버그가 다시 발생합니다.
//; 17 : printf("Direct cast value: %u\n", (unsigned int)getDouble(d));
movsd xmm0, QWORD PTR _d$[esp+76]
add esp, 4
movsd QWORD PTR [esp], xmm0
call _getDouble
call __ftol2_sse
push eax
push OFFSET $SG9261
call _printf
//; 18 : double db = getDouble(d);
movsd xmm0, QWORD PTR _d$[esp+80]
add esp, 8
movsd QWORD PTR [esp], xmm0
call _getDouble
//; 19 : printf("Indirect cast value: %u\n", (unsigned int)db);
call __ftol2_sse
push eax
push OFFSET $SG9262
call _printf
__ftol2_sse
2147483648
두 상황 모두 에서 출력 을 만드는 두 변환 모두에서 호출되며 @zwol 의혹 이 정확했습니다.
컴파일 세부 사항 :
- 명령 줄 사용 :
cl /permissive- /GS /analyze- /W3 /Gm- /Ox /sdl /D "WIN32" program.c
-
Visual Studio에서 :
-
비활성화
RTC
에 Project->
Properties->
Code Generation와 설정 기본 런타임 검사 에 기본 . -
에서 최적화를 활성화 Project
->
Properties->
Optimization하고 최적화 를 / Ox로 설정합니다 . -
디버거
x86
모드에서.
-
답변
아무도 MS의 asm을 보지 않았습니다 __ftol2_sse
.
그 결과, x87에서 부호있는 int
/ long
(Windows의 경우 32 비트 유형 모두)로 안전하게 변환되는 대신 uint32_t
.
x86 FP-> 정수 결과를 오버플로하는 정수 명령어는 줄 바꿈 / 자르기 만하 는 것이 아닙니다. 목적지에서 정확한 값을 표현할 수없는 경우 인텔이 “무한 정수”라고 부르는 것을 생성 합니다. 높은 비트 세트, 다른 비트는 지워집니다. 즉0x80000000
.
(또는 FP 유효하지 않은 예외가 마스킹되지 않으면 실행되고 값이 저장되지 않습니다. 그러나 기본 FP 환경에서는 모든 FP 예외가 마스킹됩니다. 이것이 FP 계산의 경우 오류 대신 NaN을 얻을 수있는 이유입니다.)
여기에는 fistp
(현재 반올림 모드 사용) 같은 x87 명령어와 같은 SSE2 명령어 cvttsd2si eax, xmm0
(0으로 자르기 사용, 이것이 추가 t
의미)가 모두 포함됩니다.
따라서 컴파일 double
-> unsigned
를 호출로 변환 하는 것은 버그 __ftol2_sse
입니다.
부주 / 접선 :
x86-64에서는 FP-> uint32_t를로 컴파일하여 cvttsd2si rax, xmm0
64 비트 부호있는 대상으로 변환하여 정수 대상의 하반부 (EAX)에서 원하는 uint32_t를 생성 할 수 있습니다.
결과가 0..2 ^ 32-1 범위를 벗어나면 C 및 C ++ UB이므로 큰 양의 값이나 음의 값이 정수의 비트 패턴에서 RAX (EAX)의 하위 절반을 0으로 남겨도 괜찮습니다. (정수-> 정수 변환과 달리 값의 모듈로 축소는 보장 되지 않습니다 . 음의 double을 부호없는 정수로 C 표준에 정의 된 int로 캐스트하는 동작입니까? ARM 대 x86에서 다른 동작입니다 . 명확하게 말하면 질문에 아무것도 없습니다. 정의되지 않았거나 구현 정의 된 동작입니다. FP-> int64_t가 있으면이를 사용하여 FP-> uint32_t를 효율적으로 구현할 수 있습니다. 여기에는 x87이 포함됩니다.fistp
64 비트 모드에서 64 비트 정수만 직접 처리 할 수있는 SSE2 명령어와 달리 32 비트 및 16 비트 모드에서도 64 비트 정수 대상을 쓸 수 있습니다.