2013-12-08 23 views
42

考虑下面的代码,这是我的实际问题的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-漂浮的往返至少保持正确的符号......

嗯,on ideone,输出不同的是:

2147483583 -> 2147483520 
2147483584 -> 2147483647 

做的G ++团队修复一个bug同时,还是两个输出都完全有效?

+0

我可以在x86_64上用''g ++(GCC)4.8.2 20131017(Red Hat 4.8.2-1)''确认你描述的行为(不是ideone的行为)。 –

+0

@Mat:可以证实这一点,它与哪个“-O {s,1,2,3}”无关。 –

+1

尾数太大了吗? –

回答

69

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

最接近2147483584float值为2 恰好(从整数到浮点通常舍入到最接近,其可以是向上,并且达到这种情况下的转换。具体地,该行为时从整数到浮点的转换是实现定义的,大多数实现将舍入定义为“根据FPU舍入模式”,而FPU的缺省舍入模式是舍入到最近的)。

然后,当从代表2 的浮点转换为int时,发生溢出。这种溢出是未定义的行为。一些处理器提出异常,其他处理器饱和。通常由编译器生成的IA-32指令cvttsd2si在溢出的情况下总是返回INT_MIN,无论浮动是正还是负。

即使您知道自己的目标是英特尔处理器,也不应该依赖这种行为:当针对x86-64时,编译器可以发出从浮点到整数的转换sequences of instructions that take advantage of the undefined behavior to return results other than what you might otherwise expect for the destination integer type

+0

有趣。那么我们可以从这里得出结论:ideone不能在x86上运行? :) – fredoverflow

+0

@FredOverflow在您写评论的同时,我添加了链接http://blog.frama-c.com/index.php?post/2013/10/09/Overflow-float-integer他的下半场我认为这是答案。 –

+1

感谢'x86处理器总是返回INT_MIN' - 在调试程序时很好理解。 –

10

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

在浮子32位(IEEE 754)可以将所有整数的存储从内[-2 ... 2 ]范围。范围之外的整数也可以具有精确表示,但不是全部。问题在于你只能使用24位有效位进行浮点运算。

下面是从内部 - 转换>浮通常看起来像上低电平:

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

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

64和IA-32架构软件开发人员手册

(WITH THE X87 FPU编程):

当浮点,整数或压缩BCD整数 值从存储器加载到任何x87 FPU数据寄存器中,值为 自动转换为双精度浮点扩展格式(如果它们是 尚未采用该格式)。

由于FPU寄存器是80bit的宽彩车 - 不存在具有fild这里没有问题,因为32位INT浮点格式的64位有效数字完全吻合。

到目前为止这么好。

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

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

  • 00 - 最接近偶数 - 舍入结果最接近无限精确的结果。如果两个 的值相等,则结果为偶数值(即最低有效位为0的那个值为 )。 默认
  • 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浮动的转换检查最明显的方式 - 截断(不是默认值) - 可能是这样的:

  • 记录标志
  • 否定了你的INT如果小于零
  • 最左边的1
  • 移整型发现位置向右/过程中左,使得上述结果1被定位在比特#位移的23
  • 记录号,以便可以计算指数

和代码模拟该bahavior可能看起来像这样:

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); 
} 

现在的例子 - 截断模式转换21474835832147483520

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位,并将括号尾数的明确23bits):

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

截断印章3个尾随位给我们留下67108864(67108864 + 7(3个斩波位))= 67108871(记住尽管我们用指数操作进行补偿 - 在此省略)。

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

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

67108871 = 00000100_00000000_00000000_00000111 

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

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

的“舍入到最接近的偶数”涉及找到2个数字,程序从底部托架输入值67108871和以上尽可能接近。请记住,我们仍然在80位的FPU内运行,尽管我显示一些位被移出,但它们仍在FPU寄存器中,但在存储输出值时在舍入操作期间将被移除。紧密括00000000_1.[0000000_00000000_00000000] 111 * 2^26

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

2的值是:

从顶部:

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因此更接近67108871从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 

记住那甚至只有当输入值在括号值之间的中间时,'舍入到最接近的偶数'字才有意义。只有字甚至重要和'决定'应该选择哪个括号值。在上述情况下甚至并不重要,我们必须简单地选择更接近价值,这是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 ),所以我们必须依靠甚至并选择顶部(偶数)值2147483648

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

简单的证明(没有文档引用)FPU只适用于有符号值,即。将其视为签署的每一个值是调试这样的:

unsigned int test = (1u << 31); 

_asm 
{ 
    fild [test] 
} 

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

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

+0

没有必要假设OP的编译器是以387为目标的。现代编译器针对现代英特尔指令集,将生成'cvttsd2si',它返回与其一起使用的寄存器大小的最小值(32-或64-位)溢出。 –

+1

@PascalCuoq:是的,你是对的。我的帖子足够长,不会增加更多。 – Artur