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

Posted

技术标签:

【中文标题】在 Win32 上双重转换为 unsigned int 被截断为 2,147,483,648【英文标题】:Double cast to unsigned int on Win32 is truncating to 2,147,483,648 【发布时间】:2021-01-07 00:08:31 【问题描述】:

编译如下代码:

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 documentation 中,从doubleunsigned int 的转换中没有提到有符号整数最大值。

当函数返回时,INT_MAX 以上的所有值都将被截断为 2147483648

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

我做错了吗?有没有将double 转换为unsigned int 的安全方法?

【问题讨论】:

不,你没有做错任何事(也许除了尝试使用微软的“C”编译器) Works on my machine™,在 VS2017 v15.9.18 和 VS2019 v16.4.1 上测试。使用帮助 > 发送反馈 > 报告错误来告诉他们您的版本。 我能够重现,我的结果与 OP 的结果相同。 VS2019 16.7.3. @EricPostpischil 确实是INT_MIN 的位模式 Fix pending 【参考方案1】:

编译器错误...

从@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,这是一个双精度到无符号整数转换例程...

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

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


我相信问题可能出在continuation of this from 2005 - 曾经有一个名为__ftol2 的转换函数可能适用于此代码,即它会将值转换为有符号数 -2147483647,当解释一个无符号数时会产生正确的结果。

不幸的是,__ftol2_sse 不是__ftol2 的直接替代品,因为它会 - 而不是按原样获取最低有效值位 - 通过返回 LONG_MIN 来发出超出范围错误的信号/0x80000000,在这里解释为 unsigned long 完全不是预期的。 __ftol2_sse 的行为对 signed long 有效,因为将 > LONG_MAX 的双精度值转换为 signed long 将具有未定义的行为。

【讨论】:

【参考方案2】:

在@AnttiHaapala's answer之后,我使用优化/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 suspicions 是正确的。


编译详情:

使用命令行:
cl /permissive- /GS /analyze- /W3 /Gm- /Ox /sdl /D "WIN32" program.c        

在 Visual Studio 中:

项目 中禁用 RTC -> 属性 -> 代码生成并将基本运行时检查设置为默认

Project -> 属性 -> 优化

启用优化/strong>

并将 Optimization 设置为 /Ox

x86 模式下使用调试器。

【讨论】:

有趣的是,“启用优化后,未定义的行为将真正未定义” => 代码实际上可以正常工作:F @AnttiHaapala,是的,是的,微软处于最佳状态。 应用的优化是内联,然后是常量表达式求值。它不再在运行时进行浮点到整数的转换。我想知道如果您强制getDouble 越界和/或更改它以返回编译器无法证明是常量的值,该错误是否会再次出现。 @zwol,你是对的,强制离线和阻止持续评估会导致错误恢复,但这一次是在两次转换中。【参考方案3】:

没有人看过 MS 的 __ftol2_sse 的汇编。

从结果中,我们可以推断它可能从 x87 转换为签名的int / long(Windows 上均为 32 位类型),而不是安全地转换为 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 位有符号目标,在整数目标的低半部分 (EAX) 中生成所需的 uint32_t。

如果结果在 0..2^32-1 范围之外,则为 C 和 C++ UB,因此,巨大的正值或负值将使 RAX (EAX) 的低半部分从整数不定位模式中变为零是可以的. (与整数->整数转换不同,保证值的模减少。Is the behaviour of casting a negative double to unsigned int defined in the C standard? Different behaviour on ARM vs. x86。要明确的是,问题中没有任何内容是未定义的,甚至是实现定义的行为。我只是指出,如果你有 FP->int64_t,你可以使用它来有效地实现 FP->uint32_t。这包括 x87 fistp,即使在 32 位和16位模式,不像SSE2指令在64位模式下只能直接处理64位整数。

【讨论】:

我很想查看该代码,但幸运的是我没有 MSVC... :D @AnttiHaapala:是的,我也没有 你现在可以免费下载(社区版),当然是为了研究目的

以上是关于在 Win32 上双重转换为 unsigned int 被截断为 2,147,483,648的主要内容,如果未能解决你的问题,请参考以下文章

SSE 内在函数:将 32 位浮点数转换为 UNSIGNED 8 位整数

通过 google BigQuery 将 unsigned int 转换为 signed int

C++ builder中如何把unsigned char类型转换成16进制的输出

将 unsigned char 音频转换为 short

将 errno.h 错误值转换为 Win32 GetLastError() 等效项

uint8_t uint32_t 类型强制转换出错 以及 unsigned char 类型和 unsigned int 类型相互转化