Компиляция следующего кода:
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
тех пор, пока это не возврат функции.
Я использую Visual Studio 2019 для создания программы. На gcc этого не происходит .
Я что-то делаю не так? Есть ли безопасный способ конвертировать double
в unsigned int
?
c
visual-c++
casting
x86
floating-point
Матеус Росси Сачотто
источник
источник
INT_MIN
Ответы:
Ошибка компилятора ...
Из сборки, предоставленной @anastaciu, вызывается код прямого приведения
__ftol2_sse
, который, кажется, преобразует число в длинное число со знаком . Имя подпрограммы связано сftol2_sse
тем, что это машина с поддержкой sse, но число с плавающей запятой находится в регистре с плавающей запятой 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
подпрограмму преобразования типа double в unsigned int ...Поведение прямого приведения не соответствует C89; он также не соответствует какой-либо более поздней версии - даже C89 прямо говорит, что:
Я считаю, что проблема может быть продолжением этого с 2005 года - раньше была вызываемая функция преобразования,
__ftol2
которая, вероятно, работала бы для этого кода, то есть она преобразовала бы значение в число со знаком -2147483647, что дало бы правильное результат при интерпретации беззнакового числа.К сожалению,
__ftol2_sse
это не заменяющая замена__ftol2
, как это было бы - вместо того, чтобы просто брать биты наименее значимого значения как есть - сигнализировать об ошибке вне допустимого диапазона, возвращаяLONG_MIN
/0x80000000
, который, интерпретируемый как unsigned long здесь, не находится на все, что ожидалось. Поведение__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-to-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 были правильными.Детали компиляции:
В Visual Studio:
Отключение
RTC
в Project->
Properties->
Code Generationи настройке Основных сред выполнения Проверки по умолчанию .Включение оптимизации Project
->
Properties->
Optimizationи установка параметра Оптимизация на / Ox .С отладчиком в
x86
режиме.источник
getDouble
выйдете из строя и / или измените его, чтобы вернуть значение, которое компилятор не может доказать, является постоянным.Никто не смотрел asm для MS
__ftol2_sse
.Из результата мы можем сделать вывод, что он, вероятно, преобразовал из x87 в подписанный
int
/long
(оба 32-битных типа в Windows), а не безопасно вuint32_t
.x86 FP -> целочисленные инструкции, которые переполняют целочисленный результат, не просто переносят / усекают: они производят то, что Intel называет «неопределенным целым числом», когда точное значение не может быть представлено в месте назначения: высокий бит установлен, другие биты очищены. то есть
0x80000000
.(Или, если недопустимое исключение FP не замаскировано, оно срабатывает и значение не сохраняется. Но в среде FP по умолчанию все исключения FP замаскированы. Вот почему для вычислений FP вы можете получить NaN вместо ошибки.)
Это включает в себя как инструкции x87, такие как
fistp
(с использованием текущего режима округления), так и инструкции SSE2, такие какcvttsd2si eax, xmm0
(с использованием усечения до 0, чтоt
означает дополнительное ).Так что это ошибка компиляции
double
->unsigned
преобразование в вызов__ftol2_sse
.Боковое примечание / касательная:
На x86-64 можно скомпилировать FP -> uint32_t
cvttsd2si rax, xmm0
, преобразовав его в 64-битное подписанное место назначения, создав желаемый uint32_t в младшей половине (EAX) целочисленного назначения.Это C и C ++ UB, если результат выходит за пределы диапазона 0..2 ^ 32-1, поэтому нормально, что огромные положительные или отрицательные значения оставляют нижнюю половину RAX (EAX) нулем из целочисленного неопределенного битового шаблона. (В отличие от целочисленных преобразований в целые числа, уменьшение значения по модулю не гарантируется. Определено ли поведение преобразования отрицательного числа типа double в unsigned int в стандарте C? Различное поведение на ARM по сравнению с x86 . Чтобы было ясно, ничего в вопросе является неопределенным или даже определяемым реализацией поведением. Я просто указываю, что если у вас есть FP-> int64_t, вы можете использовать его для эффективной реализации FP-> uint32_t. Это включает x87
fistp
который может записывать 64-битное целое число даже в 32-битном и 16-битном режиме, в отличие от инструкций SSE2, которые могут напрямую обрабатывать 64-битные целые числа только в 64-битном режиме.источник