在 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 中,从double
到unsigned 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>->
属性 ->
优化
在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进制的输出
将 errno.h 错误值转换为 Win32 GetLastError() 等效项
uint8_t uint32_t 类型强制转换出错 以及 unsigned char 类型和 unsigned int 类型相互转化