Двойное приведение к беззнаковому int в Win32 обрезается до 2147483648

86

Компиляция следующего кода:

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?

Матеус Росси Сачотто
источник
24
И нет, вы не делаете ничего плохого (возможно, кроме попытки использовать компилятор Microsoft "C")
Антти Хаапала
5
Работает на моей машине ™, протестировано на VS2017 v15.9.18 и VS2019 v16.4.1. Используйте «Справка»> «Отправить отзыв»> «Сообщить об ошибке», чтобы сообщить им о своей версии.
Ханс Пассан,
5
Я умею воспроизводить, у меня те же результаты, что и у ОП. VS2019 16.7.3.
Анастасиу
2
@EricPostpischil действительно, это битовый шаблонINT_MIN
Антти Хаапала
6
Ожидается исправление
Антти Хаапала

Ответы:

71

Ошибка компилятора ...

Из сборки, предоставленной @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 прямо говорит, что:

Операция остатка, выполняемая при преобразовании значения целочисленного типа в беззнаковый тип, не должна выполняться, когда значение плавающего типа преобразуется в беззнаковый тип. Таким образом, диапазон переносимых значений равен [0, Utype_MAX + 1) .


Я считаю, что проблема может быть продолжением этого с 2005 года - раньше была вызываемая функция преобразования, __ftol2которая, вероятно, работала бы для этого кода, то есть она преобразовала бы значение в число со знаком -2147483647, что дало бы правильное результат при интерпретации беззнакового числа.

К сожалению, __ftol2_sseэто не заменяющая замена __ftol2, как это было бы - вместо того, чтобы просто брать биты наименее значимого значения как есть - сигнализировать об ошибке вне допустимого диапазона, возвращая LONG_MIN/ 0x80000000, который, интерпретируемый как unsigned long здесь, не находится на все, что ожидалось. Поведение __ftol2_sseбудет действительным signed long, поскольку преобразование двойного значения> LONG_MAXв signed longбудет иметь неопределенное поведение.

Антти Хаапала
источник
23

Следуя ответу @ 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 были правильными.


Детали компиляции:

  • Используя командную строку:
cl /permissive- /GS /analyze- /W3 /Gm- /Ox /sdl /D "WIN32" program.c        
  • В Visual Studio:

    • Отключение RTCв Project -> Properties -> Code Generationи настройке Основных сред выполнения Проверки по умолчанию .

    • Включение оптимизации Project -> Properties -> Optimizationи установка параметра Оптимизация на / Ox .

    • С отладчиком в x86режиме.

Анастасиу
источник
5
Забавно, как они говорят: «Хорошо, если оптимизация включена, поведение undefined будет действительно неопределенным» => код действительно работает правильно: F
Антти Хаапала
3
@AnttiHaapala, да, да, Microsoft в лучшем виде.
Анастасиу
1
Применяемые оптимизации включали в себя встраивание, а затем оценку постоянного выражения. Он больше не выполняет преобразование типа float в int во время выполнения. Интересно, вернется ли ошибка, если вы getDoubleвыйдете из строя и / или измените его, чтобы вернуть значение, которое компилятор не может доказать, является постоянным.
zwol
1
@zwol, вы были правы, принудительное отключение и предотвращение постоянной оценки вернет ошибку, но на этот раз в обоих преобразованиях.
Анастасиу
7

Никто не смотрел 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. Это включает x87fistp который может записывать 64-битное целое число даже в 32-битном и 16-битном режиме, в отличие от инструкций SSE2, которые могут напрямую обрабатывать 64-битные целые числа только в 64-битном режиме.

Питер Кордес
источник
1
Я бы хотел изучить этот код, но, к счастью, у меня нет MSVC ...: D
Антти Хаапала
@AnttiHaapala: Да, я тоже
Питер Кордес