Win32上将double类型转换为unsigned int会截断为2,147,483,648


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”编译器之外)
Antti Haapala

5
在我的计算机上运行,​​并在VS2017 v15.9.18和VS2019 v16.4.1上进行了测试。使用帮助>发送反馈>报告错误来告诉他们您的版本。
汉斯·帕桑

5
我能够复制,与OP的结果相同。VS2019 16.7.3。
anastaciu

2
@EricPostpischil确实是INT_MIN
Antti Haapala

Answers:


71

编译器错误...

通过@anastaciu提供的汇编,直接强制__ftol2_sse转换代码调用,这似乎将数字转换为带符号的long。例程名称是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

它弹出并将double值存储到本地变量,然后将其加载到SSE寄存器中并调用__dtoui3它是double到unsigned int转换例程。

直接演员表的行为不符合C89;也不符合任何更高版本-甚至C89都明确指出:

当将整数类型的值转换为无符号类型时,无需执行剩余操作,而将浮点类型的值转换为无符号类型时,则无需执行其余操作。因此,可移植值的范围为[0,Utype_MAX +1)


我认为问题可能是从2005年开始的延续-曾经有一个转换函数调用__ftol2,该函数可能对此代码有效,即它将值转换为带符号的数字-2147483647,这将产生正确的解释为无符号数字时的结果。

不幸的__ftol2_sse__ftol2,它不是的替代品,因为它不是-只是将最低有效的值原样保持原状,而是通过返回LONG_MIN/来表示超出范围的错误0x80000000,该符号在这里不解释为无符号,而不是在所有的预期。的行为__ftol2_sse是有效的signed long,作为双值转换>LONG_MAXsigned 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()并添加了常量表达式评估,从而消除了运行时进行转换的需要,从而使错误消失了。

出于好奇,我进行了更多测试,即更改代码以在运行时强制将浮点数转换为整数。在这种情况下,结果仍然是正确的,经过优化的编译器将__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中:

    • 禁止RTCProject -> Properties -> Code Generation与设置基本运行时检查默认

    • 在启用优化Project -> Properties -> Optimization和设置优化/ OX

    • 在调试器x86模式下。


5
有趣的是,他们的感觉是“启用优化后,未定义的行为实际上是未定义的” =>代码实际上正常工作:F
Antti Haapala

3
@AnttiHaapala,是的,是的,微软最好的。
anastaciu

1
应用的优化是内联,然后进行常量表达式评估。它不再在运行时进行浮点到整数转换。我想知道如果您强行getDouble退出和/或更改它以返回编译器无法证明恒定的值,该错误是否再次出现。
zwol

1
@zwol,您是对的,强制脱机并防止进行持续评估会将错误带回来,但这一次是两次转换。
anastaciu

7

没人看过MS的组件__ftol2_sse

根据结果​​,我们可以推断出它可能是从x87转换为有符号int/ long(在Windows上均为32位类型),而不是安全地转换为uint32_t

x86 FP->溢出整数结果的整数指令不仅会包装/截断:当目标中无法表示确切值时,它们会产生Intel所谓的“整数不确定”高位置1,其他位清零。即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位带符号的目标,从而在整数目标的下半部分(EAX)中生成所需的uint32_t。

如果结果超出0..2 ^ 32-1范围,则为C和C ++ UB,因此可以肯定的是,较大的正值或负值将使整数不确定位模式的RAX(EAX)的下半部分变为零。(与整数->整数转换不同,不能保证值的模减少。 在C标准中是否定义了将负双精度数转换为无符号int的行为?ARM与x86的行为不同。要清楚,问题不存在是未定义的,甚至不是实现定义的行为。我只是指出,如果您具有FP-> int64_t,则可以使用它来有效地实现FP-> uint32_t。其中包括x87fistp 与SSE2指令不同,后者只能在64位模式下直接处理64位整数,即使在32位和16位模式下,它也可以写入64位整数目标。


1
我很想研究一下这些代码,但是幸运的是我没有MSVC ...:D
Antti Haapala

@AnttiHaapala:是的,我也不是
Peter Cordes
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.