从int到float并返回时签名发生变化

考虑下面的代码,这是我的实际问题的SSCCE :

#include <iostream> int roundtrip(int x) { return int(float(x)); } int main() { int a = 2147483583; int b = 2147483584; std::cout << a << " -> " << roundtrip(a) << '\n'; std::cout << b << " -> " << roundtrip(b) << '\n'; } 

我电脑上的输出(Xubuntu 12.04.3 LTS)是:

 2147483583 -> 2147483520 2147483584 -> -2147483648 

请注意,在往返之后,正数b如何结束的。 这种行为是否被很好地指定? 我本来希望int-to-float往返至less能保持正确的符号…

嗯, 在ideone上 ,输出是不同的:

 2147483583 -> 2147483520 2147483584 -> 2147483647 

在此期间,g ++团队是否修复了一个bug,或者这两个输出都是完全有效的?

您的程序正在调用未定义的行为,因为从浮点到整数的转换溢出。 你看到的只是x86处理器上的常见症状。

最接近2147483584float值是2147483584 (整数到浮点的转换通常是四舍五入到最接近的,可以是up,在这种情况下是up的,具体来说,点是实现定义的,大多数实现将舍入定义为“根据FPU舍入模式”,而FPU的缺省舍入模式是舍入到最近的)。

然后,在从代表2 31的float转换为int ,发生溢出。 这个溢出是未定义的行为。 一些处理器引起exception,其他处理器饱和。 通常由编译器生成的IA-32指令cvttsd2si在溢出的情况下总是返回INT_MIN ,而不pipe浮动是正还是负。

即使您知道自己的目标是英特尔处理器,也不应该依赖这种行为:当针对x86-64时,编译器可以发出从浮点转换为整数的指令序列,以利用未定义的行为返回除了你可能期望的目标整数types以外的结果 。

帕斯卡的答案是确定的 – 但缺乏细节,这需要一些用户不明白;-)。 如果你对它在较低级别上看起来感兴趣(假设协处理器而不是软件处理浮点操作),请继续阅读。

在32位浮点数(IEEE 754)中,可以存储[-2 24 … 2 24 ]范围内的所有整数。 整数范围之外的整数也可以具有精确的浮点数,但不是全部。 问题是你可以只有24个有效位在浮动。

下面是从int-> float的转换通常看起来像在低级别:

 fild dword ptr[your int] fstp dword ptr[your float] 

它只是2个协处理器指令的序列。 首先将32位int加载到协处理器的堆栈上,并将其转换为80位宽的浮点数。

英特尔®64和IA-32架构软件开发人员手册

(使用X87 FPU进行编程):

当浮点,整数或打包的BCD整数值从存储器加载到任何x87 FPU数据寄存器中时,这些值将自动转换为双精度浮点扩展格式(如果它们尚不是该格式的话)。

由于FPU寄存器是80位宽的浮点数,所以在这里没有问题,因为32位int完全适合浮点格式的64位有效数。

到现在为止还挺好。

第二部分 – fstp有点棘手,可能会令人惊讶。 它应该存储在32位浮点80位浮点。 虽然它是关于整数值(在问题中),协处理器可能实际上执行“舍入”。 柯? 即使以浮点格式存储整数值又怎么样? ;-)。

我会尽快解释 – 首先看看x87提供的舍入模式(它们是IEE 754舍入模式的化身)。 X87 fpu有4个舍入模式,由fpu控制字的#10和#11位控制:

  • 00 – 到最近偶数 – 圆形结果是最接近无限精确的结果。 如果两个值相等,则结果是偶数值(也就是最低位为零的那个)。 默认
  • 01 – 朝-Inf
  • 10 – 往+ inf
  • 11 – 向0(即截断)

你可以使用这个简单的代码使用舍入模式(虽然它可以做不同的 – 在这里显示低水平):

 enum ROUNDING_MODE { RM_TO_NEAREST = 0x00, RM_TOWARD_MINF = 0x01, RM_TOWARD_PINF = 0x02, RM_TOWARD_ZERO = 0x03 // TRUNCATE }; void set_round_mode(enum ROUNDING_MODE rm) { short csw; short tmp = rm; _asm { push ax fstcw [csw] mov ax, [csw] and ax, ~(3<<10) shl [tmp], 10 or ax, tmp mov [csw], ax fldcw [csw] pop ax } } 

好吧,但仍是如何与整数值相关? 耐心…了解为什么你可能需要四舍五入模式涉及int到浮点数转换检查最明显的方式将int转换为float – 截断(不是默认值) – 可能看起来像这样:

  • logging标志
  • 否定你的int如果小于零
  • find最左边的位置1
  • 将int移到右边/左边,使得上面find的1位于位#23上
  • logging过程中的class次数,以便计算指数

而模拟这个行为的代码可能是这样的:

 float int2float(int value) { // handles all values from [-2^24...2^24] // outside this range only some integers may be represented exactly // this method will use truncation 'rounding mode' during conversion // we can safely reinterpret it as 0.0 if (value == 0) return 0.0; if (value == (1U<<31)) // ie -2^31 { // -(-2^31) = -2^31 so we'll not be able to handle it below - use const value = 0xCF000000; return *((float*)&value); } int sign = 0; // handle negative values if (value < 0) { sign = 1U << 31; value = -value; } // although right shift of signed is undefined - all compilers (that I know) do // arithmetic shift (copies sign into MSB) is what I prefer here // hence using unsigned abs_value_copy for shift unsigned int abs_value_copy = value; // find leading one int bit_num = 31; int shift_count = 0; for(; bit_num > 0; bit_num--) { if (abs_value_copy & (1U<<bit_num)) { if (bit_num >= 23) { // need to shift right shift_count = bit_num - 23; abs_value_copy >>= shift_count; } else { // need to shift left shift_count = 23 - bit_num; abs_value_copy <<= shift_count; } break; } } // exponent is biased by 127 int exp = bit_num + 127; // clear leading 1 (bit #23) (it will implicitly be there but not stored) int coeff = abs_value_copy & ~(1<<23); // move exp to the right place exp <<= 23; int ret = sign | exp | coeff; return *((float*)&ret); } 

现在,示例 – 截断模式将2147483583转换为2147483520

 2147483583 = 01111111_11111111_11111111_10111111 

在int-> float转换期间,您必须将最左边的1移到位#23。 现在领先1位在#30。 为了把它放在#23位,你必须右移7个位置。 在这期间,你松了(他们不适合在32位浮点格式)从右边的7个LSB位(你截断/砍)。 他们是:

 01111111 = 63 

63是什么原始数字丢失:

 2147483583 -> 2147483520 + 63 

截断很容易,但不一定是你想要的和/或最适合所有情况。 考虑下面的例子:

 67108871 = 00000100_00000000_00000000_00000111 

上面的值不能被float完全表示,但是检查截断对它做了什么。 如前所述 – 我们需要将最左边的1移到第23位。 这需要将值正好移位3个位置,丢失3个LSB位(截至目前为止,我将写入不同的数字,以显示浮点的第24位是什么位置,并将明确表示23位有效位):

 00000001.[0000000_00000000_00000000] 111 * 2^26 (3 bits shifted out) 

截断3尾随我们67108864 (67108864 + 7(3斩波比特))= 67108871(记住,虽然我们移动我们补偿与指数操作 – 这里省略)。

这够好吗? 嘿67108872完全可以用32位浮点数表示,应该比67108864 ? 正确,这是你可能想要谈论四舍五入时转换为32位浮点数。

现在让我们来看看默认的“舍入到最近的偶数”模式是如何工作的,以及OP在其中的含义是什么。 再考虑一下相同的例子。

 67108871 = 00000100_00000000_00000000_00000111 

正如我们所知道的,我们需要3个右移位来放置最左边的1#23:

 00000000_1.[0000000_00000000_00000000] 111 * 2^26 (3 bits shifted out) 

“舍入到最近偶数”的过程涉及从底部和上面find包含input值67108871 2个数字尽可能接近。 请记住,我们仍然在80位的FPU内运行,所以虽然我显示了一些位被移出,但它们仍在FPU寄存器中,但是在存储输出值时在舍入操作期间将被移除。

 00000000_1.[0000000_00000000_00000000] 111 * 2^26 (3 bits shifted out) 

2的值紧密地包围在00000000_1.[0000000_00000000_00000000] 111 * 2^26是:

从顶部:

  00000000_1.[0000000_00000000_00000000] 111 * 2^26 +1 = 00000000_1.[0000000_00000000_00000001] * 2^26 = 67108872 

并从下面:

  00000000_1.[0000000_00000000_00000000] * 2^26 = 67108864 

显然6710887267108864更接近67108864因此从32位int值67108871转换为67108872 (舍入到最接近的偶数模式)。

现在OP的数字(仍然四舍五入到最近):

  2147483583 = 01111111_11111111_11111111_10111111 = 00000000_1.[1111111_11111111_11111111] 0111111 * 2^30 

括号值:

最佳:

  00000000_1.[1111111_111111111_11111111] 0111111 * 2^30 +1 = 00000000_10.[0000000_00000000_00000000] * 2^30 = 00000000_1.[0000000_00000000_00000000] * 2^31 = 2147483648 

底部:

 00000000_1.[1111111_111111111_11111111] * 2^30 = 2147483520 

请记住, 即使在“舍入到最接近的偶数”中,只有当input值在括号值之间的中间时才是重要的。 只有这个词才是重要的,并且“决定”应该select哪一个括号。 在上述情况下甚至无所谓,我们只需要select更接近的值,即2147483520

最后一个OP的案例显示了即使是单词也很重要的问题。 :

  2147483584 = 01111111_11111111_11111111_11000000 = 00000000_1.[1111111_11111111_11111111] 1000000 * 2^30 

括号的值与以前相同:

顶部: 00000000_1.[0000000_00000000_00000000] * 2^31 = 2147483648

底部: 00000000_1.[1111111_111111111_11111111] * 2^30 = 2147483520

现在没有更接近的价值(2147483648-2147483584 = 64 = 2147483584-2147483520),所以我们必须依靠,并select顶部(偶数)值2147483648

而这里OP的问题在于Pascal曾经简要描述过。 FPU只能用于有符号值, 2147483648不能存储为有符号整数,因为它的最大值是2147483647因此问题。

简单的certificate(没有文档引用),FPU只在签名的值上工作。 将每个值视为签名是通过debugging这个:

 unsigned int test = (1u << 31); _asm { fild [test] } 

虽然看起来testing值应该被视为未签名,但将被加载为-2 31,因为没有单独的指令将有符号值和无符号值加载到FPU中。 同样,你不会find指令,允许你将无符号的值从FPU存储到mem。 无论您如何在程序中声明它,所有内容都被视为已签名模式。

很长,但希望有人会从中学到一些东西。