从int转到float并返回时,符号会发生变化

fre*_*low 42 c++ floating-point type-conversion ieee-754 twos-complement

考虑以下代码,这是我实际问题的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';
}
Run Code Online (Sandbox Code Playgroud)

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

2147483583 -> 2147483520
2147483584 -> -2147483648
Run Code Online (Sandbox Code Playgroud)

请注意b往返后正数如何变为负数.这种行为是否明确规定?我原本期望int-to-float round-tripping至少能正确保存符号......

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

2147483583 -> 2147483520
2147483584 -> 2147483647
Run Code Online (Sandbox Code Playgroud)

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

Pas*_*uoq 68

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

float最接近的值2147483584是2 31(从整数到浮点的转换通常是舍入到最近的,可以是向上的,在这种情况下是向上的.具体来说,从整数转换为浮点时的行为是实现定义,大多数实现将舍入定义为"根据FPU舍入模式",并且FPU的默认舍入模式是舍入到最近的).

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

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

  • 感谢“x86 处理器总是返回 INT_MIN”——在调试程序时很高兴知道这一点。 (2认同)

Art*_*tur 10

帕斯卡的答案是可以的 - 但缺乏细节,这需要一些用户不能得到它;-).如果您对它在较低级别上的外观感兴趣(假设协处理器而不是软件处理浮点运算) - 请继续阅读.

在32位浮点数(IEEE 754)中,您可以存储[-2 24 ... 2 24 ]范围内的所有整数.范围之外的整数也可能具有精确的浮动表示,但并非所有整数都具有浮点数.问题是你在float中只能有24个有效位.

以下是int-> float的转换通常在低级别上的转换:

fild dword ptr[your int]
fstp dword ptr[your float]
Run Code Online (Sandbox Code Playgroud)

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

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

(使用X87 FPU编程):

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

由于FPU寄存器是80位宽的浮点数 - 这里没有问题,fild因为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
    }
}
Run Code Online (Sandbox Code Playgroud)

好的但是仍然如何与整数值相关?耐心......理解为什么你可能需要在int到float转换中涉及的舍入模式检查最明显的将int转换为float的方法 - 截断(不是默认) - 可能如下所示:

  • 记录标志
  • 如果小于零则否定你的int
  • 找到最左边1的位置
  • 向右/向左移动int,使得上面找到的1位于第23位
  • 记录过程中的班次数,以便计算指数

模拟这个行为的代码可能如下所示:

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

现在示例 - 截断模式转换21474835832147483520.

2147483583 = 01111111_11111111_11111111_10111111
Run Code Online (Sandbox Code Playgroud)

在int-> float转换期间,您必须将最左边的1移到第23位.现在领先1位于第30位.为了将它放在第23位,你必须右移7个位置.在此期间你松开(它们不适合32位浮点格式)右边的7 lsb位(截断/切断).他们是:

01111111 = 63
Run Code Online (Sandbox Code Playgroud)

并且63是原始数字丢失的:

2147483583 -> 2147483520 + 63
Run Code Online (Sandbox Code Playgroud)

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

67108871 = 00000100_00000000_00000000_00000111
Run Code Online (Sandbox Code Playgroud)

上面的值不能用float精确表示,但要检查截断对它的作用.如前所述 - 我们需要将最左边的1移到第23位.这需要将值正好向右移动3个位置,而不是3个LSB位(截至现在,我将以不同的方式编写数字,显示浮点的隐式第24位,并将包含显式的23位有效数字):

00000001.[0000000_00000000_00000000] 111 * 2^26 (3 bits shifted out)
Run Code Online (Sandbox Code Playgroud)

截断67108864斩波3个尾随位(67108864 + 7(3个斩波位))= 67108871(记住我们移位时我们用指数操作补偿 - 这里省略).

这够好吗?嘿67108872,32位漂浮是完全可以代表的,应该比67108864右边好多少?CORRECT,在将int转换为32位浮点数时,您可能想要讨论舍入.

现在让我们看看默认'舍入到最接近的偶数'模式是如何工作的,以及它对OP的影响是什么.再一次考虑相同的例子.

67108871 = 00000100_00000000_00000000_00000111
Run Code Online (Sandbox Code Playgroud)

正如我们所知,我们需要3个右移,将最左边的1个放在第23位:

00000000_1.[0000000_00000000_00000000] 111 * 2^26 (3 bits shifted out)
Run Code Online (Sandbox Code Playgroud)

"舍入到最近的偶数"的过程包括找到2个数字,这些数字包括67108871从底部和上方尽可能接近的输入值.请记住,我们仍然在FPU上运行80位,因此虽然我显示一些位被移出,但它们仍然在FPU reg中,但在存储输出值时在舍入操作期间将被移除.

00000000_1.[0000000_00000000_00000000] 111 * 2^26 (3 bits shifted out)
Run Code Online (Sandbox Code Playgroud)

紧密00000000_1.[0000000_00000000_00000000] 111 * 2^26包含的2个值是:

从顶部:

  00000000_1.[0000000_00000000_00000000] 111 * 2^26
                                     +1
= 00000000_1.[0000000_00000000_00000001] * 2^26 = 67108872
Run Code Online (Sandbox Code Playgroud)

从下面:

  00000000_1.[0000000_00000000_00000000] * 2^26 = 67108864
Run Code Online (Sandbox Code Playgroud)

显然67108872是更接近6710887167108864因此从32位整数值转换67108871给出67108872(在舍入到最接近的偶数模式).

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

 2147483583 = 01111111_11111111_11111111_10111111
= 00000000_1.[1111111_11111111_11111111] 0111111 * 2^30
Run Code Online (Sandbox Code Playgroud)

括号值:

最佳:

  00000000_1.[1111111_111111111_11111111] 0111111 * 2^30
                                      +1
= 00000000_10.[0000000_00000000_00000000] * 2^30
=  00000000_1.[0000000_00000000_00000000] * 2^31 = 2147483648
Run Code Online (Sandbox Code Playgroud)

底部:

00000000_1.[1111111_111111111_11111111] * 2^30 = 2147483520
Run Code Online (Sandbox Code Playgroud)

请记住,即使输入值位于括号值之间," 即使舍入到最接近的偶数"中的单词也很重要.只有这样一句话甚至事项和"决定",这架价值应该被选中.在上述情况下,即使无关紧要,我们也必须选择更接近的价值,即2147483520

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

 2147483584 = 01111111_11111111_11111111_11000000
= 00000000_1.[1111111_11111111_11111111] 1000000 * 2^30
Run Code Online (Sandbox Code Playgroud)

括号值与以前相同:

最佳: 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不能存储为signed int,因为其最大值为2147483647因此会出现问题.

简单证明(没有文档引用)FPU仅适用于有符号值,即.通过调试来处理签名的每个值:

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

_asm
{
    fild [test]
}
Run Code Online (Sandbox Code Playgroud)

虽然看起来测试值应该被视为无符号,但它将被加载为-2 31,因为没有单独的指令将有符号和无符号值加载到FPU中.同样,您也找不到允许您将无符号值从FPU存储到mem的指令.无论您如何在程序中声明它,一切都只是被视为签名的一点模式.

很久但希望有人能从中学到一些东西.