Подвійна передача до беззнакового int на Win32 зменшується до 2 147 483 688


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
І ні, ви не робите нічого поганого (можливо, крім спроби скористатися компілятором "C" від Microsoft)
Антті Хаапала

5
Працює на моїй машині ™, протестовано на VS2017 v15.9.18 та VS2019 v16.4.1. Скористайтеся Довідкою> Надіслати відгук> Повідомити про помилку, щоб повідомити їх про свою версію.
Ганс Пасант,

5
Я здатний відтворюватись, маю ті самі результати, що й результати OP. VS2019 16.7.3.
anastaciu

2
@EricPostpischil справді, це біт-шаблонINT_MIN
Антті Хаапала,

Відповіді:


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що є подвійною процедурою перетворення int без підпису ...

Поведінка прямого складу не відповідає C89; і це не відповідає будь-якій пізнішій редакції - навіть C89 прямо говорить, що:

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


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

На жаль, __ftol2_sseце не заміна заміни __ftol2, оскільки вона - замість того, щоб просто брати найменш значущі біти значення як є - сигналізує про помилку поза діапазоном, повертаючи LONG_MIN/ 0x80000000, що, інтерпретоване як непідписане 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
Забавно, як вони схожі на "нормально з увімкненими оптимізаціями, невизначена поведінка буде дійсно невизначеною" => код насправді працює правильно: F
Антті Хаапала

3
@AnttiHaapala, так, так, Microsoft у найкращому вигляді.
anastaciu

1
Застосованими оптимізаціями були вбудовані, а потім постійні оцінки виразів. Це більше не робить конвертацію float-to-int під час виконання. Цікаво, чи помилка повертається, якщо змусити getDoubleвийти за межі рядка та / або змінити її, щоб повернути значення, яке компілятор не може довести, постійне.
zwol

1
@zwol, ти мав рацію, примусове виведення з ладу та запобігання постійному оцінюванню поверне помилку назад, але цього разу в обох перетвореннях.
anastaciu

7

Ніхто не розглядав склад 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) нульовою від цілочисельного невизначеного бітового шаблону. (На відміну від цілочисельних-> цілочисельних перетворень, модульне зменшення значення не гарантується. Чи визначено поведінку відливання подвійного до беззнакового int у стандарті C? Інша поведінка на ARM проти x86 . Щоб бути зрозумілим, нічого в питанні є невизначеною або навіть визначеною реалізацією поведінкою. Я лише вказую, що якщо у вас є FP-> int64_t, ви можете використовувати його для ефективної реалізації FP-> uint32_t. Сюди входить x87fistp який може писати 64-розрядне ціле призначення навіть у 32-розрядному та 16-розрядному режимах, на відміну від інструкцій SSE2, які можуть безпосередньо обробляти 64-розрядні цілі числа у 64-розрядному режимі.


1
Я хотів би розглянути цей код, але, на щастя, у мене немає MSVC ...: D
Антті Хаапала

@AnttiHaapala: Так, ні я,
Пітер Кордес,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.