从int到float和b时符号更改

考虑以下代码,这是我的实际问题的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到float的往返至少能够正确保留符号...

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

2147483583 -> 2147483520
2147483584 -> 2147483647

g ++小组是在同时修复了一个错误,还是两个输出都完全有效?

2个解决方案
68 votes

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

最接近cvttsd2siint值恰好是231(从整数到浮点的转换通常四舍五入到最接近的值,该值可能会上升,并且在这种情况下会上升。具体而言,从整数到浮点的转换行为 是根据实现定义的,大多数实现将舍入定义为“根据FPU舍入模式”,而FPU的默认舍入模式是舍入到最接近的值)。

然后,当从代表231的浮点数转换为int时,发生溢出。 此溢出是未定义的行为。 一些处理器引发异常,另一些则达到饱和。 通常由编译器生成的IA-32指令cvttsd2si碰巧总是在溢出的情况下返回INT_MIN,无论浮点是正数还是负数。

即使知道目标是Intel处理器,也不应依赖此行为:当目标对象为x86-64时,编译器可以发出利用未定义行为返回的指令序列,以将浮点数转换为整数。 结果,而不是目标整数类型可能期望的结果。

Pascal Cuoq answered 2020-08-12T04:59:56Z
10 votes

Pascal的回答是可以的-但缺少详细信息,这意味着某些用户无法获得它;-)。 如果您对它在较低层的外观感兴趣(假设协处理器而不是软件处理浮点操作),请继续阅读。

在32位浮点数(IEEE 754)中,您可以存储[-224 ... 224]范围内的所有整数。 范围之外的整数也可能具有浮点数的精确表示,但并非所有整数都具有。 问题在于,在float中只能使用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位宽的浮点数-2147483648没有问题,因为32位int完全适合64位有效浮点格式。

到目前为止,一切都很好。

第二部分-2147483648有点棘手,可能令人惊讶。 应该将80位浮点存储在32位浮点中。 尽管这全都与整数值有关(在问题中),但协处理器实际上可以执行“舍入”。 ? 即使整数值以浮点格式存储,您如何舍入整数值? ;-)。

稍后我将对其进行解释-首先让我们看看x87提供的舍入模式(它们是IEE 754舍入模式的化身)。 X87 fpu具有4种舍入模式,由fpu控制字的位#10和#11控制:

  • 00-最接近偶数-舍入后的结果最接近于无限精确的结果。 如果两个值相等,结果是偶数值(即最低有效位为零的1)。 默认
  • 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到float转换中涉及的舍入模式检查将int转换为float的最明显方法-截断(不是默认值)-可能看起来像这样:

  • 记录标志
  • 如果小于零,则求反
  • 找到最左边的位置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);
}

现在示例-截断模式将2147483648转换为67108871

2147483583 = 01111111_11111111_11111111_10111111

在int-> float转换期间,必须将最左1移至#23位。 现在第1位在第30位。 为了将其放置在位#23中,您必须右移7个位置。 在此期间,您从右边松开(它们将不适合32bit浮点格式)(从右至7)截断/截断。 他们是:

01111111 = 63

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

2147483583 -> 2147483520 + 63

截断很容易,但不一定是您想要的和/或在所有情况下都是最佳的。 考虑以下示例:

67108871 = 00000100_00000000_00000000_00000111

不能用浮点数精确表示上述值,但请检查截断会对其进行什么处理。 如前所述-我们需要将最左1移至#23位。 这要求将值精确地向右移动3个位置,从而损失3个LSB位(到目前为止,我将写一些不同的数字,以显示float的隐式第24位在哪里,并将包围有效的23位显式)。

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

截断可截断3个尾随位,并留给我们2147483648(67108864 + 7(3个切碎的位))= 67108871(请记住,尽管我们进行移位,但我们使用指数操作进行了补偿-在此省略)。

这样够好吗? 嘿2147483648可以完美地用32位浮点数表示,应该比67108871好得多,对吗? 正确,这是在将int转换为32bit float时可能要讨论的舍入的地方。

现在,让我们看看默认的“四舍五入到最接近的偶数”模式如何工作,以及在OP的情况下有什么含义。 再考虑一次相同的示例。

67108871 = 00000100_00000000_00000000_00000111

众所周知,我们需要3次右移才能将第23位的最左1放置:

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

“四舍五入到最接近的偶数”的过程涉及找到2个数字,这些数字应尽可能从底部和上方包围输入值2147483648。 请记住,我们仍在FPU上以80位运行,因此尽管我展示了一些移出的位仍在FPU reg中,但是在存储输出值时在舍入操作期间将被删除。

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

2147483648紧密相关的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

显然214748364867108871更接近67108871,因此从32位整数值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

请记住,仅当输入值介于方括号值之间时,“四舍五入至最接近的偶数”中的偶数才有意义。 只有这样,单词才变得重要,并“决定”应选择哪个括号值。 在上述情况下甚至没有关系,我们必须简单地选择较接近的值,即2147483648

最后一个OP的情况表明了连字都重要的问题。 :

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

括号值与以前相同:

顶部:2147483648

底部:2147483648

现在没有更接近的值(2147483648-2147483584 = 64 = 2147483584-2147483520),因此我们必须依靠偶数并选择最高(偶数)值2147483648

OP的问题在于Pascal曾简要描述过。 FPU仅适用于带符号的值,并且2147483648不能存储为带符号的int,因为其最大值为2147483647,因此会出现问题。

简单的证明(没有文档引号)表明FPU仅适用于带符号的值,即。 通过调试以下命令将每个值视为带符号:

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

_asm
{
    fild [test]
}

尽管看起来测试值应该被视为无符号,但由于没有单独的说明将有符号和无符号值加载到FPU中,因此它将被加载为-231。 同样,您将找不到允许您将FPU中的未签名值存储到mem的说明。 不管您在程序中如何声明所有内容,所有内容都只会被视为已签名的模式。

很长一段时间,但希望有人能从中学到一些东西。

Artur answered 2020-08-12T05:04:05Z
translate from https://stackoverflow.com:/questions/20453449/sign-changes-when-going-from-int-to-float-and-back