在 Win32 上双转换为 unsigned int 被截断为 2,147,483,648

Mat*_*tto 85 c floating-point x86 casting visual-c++

编译以下代码:

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;
}
Run Code Online (Sandbox Code Playgroud)

输出 (MSVC x86):

INT_MAX: 2147483647
UINT_MAX: 4294967295
Double value: 2147483649.000000
Direct cast value: 2147483648
Indirect cast value: 2147483649
Run Code Online (Sandbox Code Playgroud)

输出 (MSVC x64):

INT_MAX: 2147483647
UINT_MAX: 4294967295
Double value: 2147483649.000000
Direct cast value: 2147483649
Indirect cast value: 2147483649
Run Code Online (Sandbox Code Playgroud)

Microsoft 文档中,没有提到从double到 的转换中的有符号整数最大值unsigned int

当它是函数的返回时,上面的所有值INT_MAX都被截断2147483648

我正在使用Visual Studio 2019来构建程序。这不会发生在gcc 上

我做错了什么吗?有没有安全的转换double方式unsigned int

Ant*_*ala 70

编译器错误...

从@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
Run Code Online (Sandbox Code Playgroud)

另一方面,间接转换确实

; 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
Run Code Online (Sandbox Code Playgroud)

它将双精度值弹出并存储到局部变量,然后将其加载到 SSE 寄存器中并调用__dtoui3它是双精度到无符号整数的转换例程...

直接强制转换的行为不符合 C89;它也不符合任何后来的修订版——甚至C89 也明确表示:

将浮点类型值转换为无符号类型时,无需进行整数类型值转换为无符号类型时的余数运算。因此可移植值的范围是[0, Utype_MAX + 1)


我相信这个问题可能是2005年的延续- 曾经有一个转换函数被调用__ftol2,它可能适用于这个代码,即将值转换为有符号数-2147483647,它会产生正确的解释无符号数时的结果。

不幸的__ftol2_sse__ftol2,它不是 的直接替代品,因为它会 - 而不是按原样采用最低有效值位 - 通过返回LONG_MIN/来表示超出范围的错误0x80000000,这在此处被解释为 unsigned long 不是一切都在预料之中。的行为__ftol2_sse是有效的signed long,作为双值转换>LONG_MAXsigned long会有未定义的行为。


ana*_*ciu 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
Run Code Online (Sandbox Code Playgroud)

优化内联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
Run Code Online (Sandbox Code Playgroud)

但是,防止内联会__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
Run Code Online (Sandbox Code Playgroud)

__ftol2_sse在两种转换中都调用了2147483648在两种情况下的输出,@zwol 怀疑是正确的。


编译详情:

  • 使用命令行:
cl /permissive- /GS /analyze- /W3 /Gm- /Ox /sdl /D "WIN32" program.c        
Run Code Online (Sandbox Code Playgroud)
  • 在 Visual Studio 中:

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

    • 启用优化Project -> Properties -> Optimization并将优化设置为 /Ox

    • 在调试器x86模式下。

  • 有趣的是,他们就像“启用优化后,未定义的行为将真正未定义”=>代码实际上工作正常:F (5认同)
  • @AnttiHaapala,是的,是的,微软是最好的。 (3认同)
  • 应用的优化是内联,然后是常量表达式评估。它不再在运行时进行浮点到整数的转换。我想知道如果您强制“getDouble”超出范围和/或更改它以返回编译器无法证明是常量的值,该错误是否会再次出现。 (2认同)

Pet*_*des 8

没有人看过 MS 的 asm __ftol2_sse

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

x86 FP -> 溢出整数结果的整数指令不只是包装/截断:当目标中无法表示确切值时,它们会产生英特尔所谓的“整数不确定”高位设置,其他位清除。即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) 的低半部分为零。(与 integer->integer 转换不同,不能保证 值的模减少。C 标准中定义了将负双精度转换为 unsigned int 的行为吗?ARM 与 x86 上的不同行为。要清楚,问题中没有任何内容是未定义的甚至是实现定义的行为。我只是指出,如果你有 FP->int64_t,你可以用它来有效地实现 FP->uint32_t。这包括 x87fistp 即使在 32 位和 16 位模式下也可以写入 64 位整数目标,不像 SSE2 指令只能在 64 位模式下直接处理 64 位整数。