【问题标题】:sign changes when going from int to float and back从 int 到 float 并返回时符号发生变化
【发布时间】:2013-12-25 13:09:58
【问题描述】:

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

嗯,on ideone,输出不一样:

2147483583 -> 2147483520
2147483584 -> 2147483647

g++ 团队是否同时修复了一个错误,或者两个输出都完全有效?

【问题讨论】:

  • 我可以在 x86_64 上使用 g++ (GCC) 4.8.2 20131017 (Red Hat 4.8.2-1) 确认您描述的行为(不是 ideone 行为)。
  • @Mat: 可以确认,不管-O{s,1,2,3}是哪个。
  • 尾数是否太大?
  • 对我来说,这似乎是整数通常不能用浮点数表示的事实和整数溢出/环绕的混合体。尝试在roundtrip()中输出临时变量。

标签: c++ floating-point type-conversion ieee-754 twos-complement


【解决方案1】:

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

最接近2147483584float 值正好是231(从整数到浮点的转换通常四舍五入到最接近的,可以向上,在这种情况下向上. 具体来说,整数到浮点数的转换行为是实现定义的,大部分实现都将舍入定义为“根据FPU的舍入方式”,而FPU的默认舍入方式是四舍五入到最接近的)。

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

即使您知道您的目标是 Intel 处理器,也不应该依赖此行为:当目标是 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,用于从浮点到整数的转换。

【讨论】:

  • 有趣。那么我们可以由此得出结论,ideone 不能在 x86 上运行吗? :)
  • @FredOverflow 在你写评论的同时,我正在添加指向blog.frama-c.com/index.php?post/2013/10/09/… 的链接,我认为它的后半部分可以回答这个问题。
  • 感谢“x86 处理器碰巧总是返回 INT_MIN”——在调试程序时很高兴知道。
  • -fsanitize=float-cast-overflow 传递给clang 将在运行时捕获任何这种情况。 clang.llvm.org/docs/…
  • @strcat 在我彻底测试之前,我不会假设-fsanitize=float-cast-overflow 会捕获所有这样的溢出。很难将健全的运行时检查添加到从一开始就不是为此而设计的编译器,因为前端和现有优化可能会干扰。但它肯定能赶上这个普通的。
【解决方案2】:

Pascal 的回答是可以的 - 但缺乏细节,这意味着一些用户没有得到它;-)。如果您对它在较低级别上的外观感兴趣(假设协处理器而不是软件处理浮点运算) - 请继续阅读。

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

到目前为止一切顺利。

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

我将很快解释它 - 让我们首先看看 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 到 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);
}

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

2147483583 = 01111111_11111111_11111111_10111111

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

01111111 = 63

而 63 是丢失的原始数字:

2147483583 -> 2147483520 + 63

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

67108871 = 00000100_00000000_00000000_00000111

上面的值不能用浮点数精确表示,但请检查截断对它的作用。如前所述 - 我们需要将最左边的 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 好得多,对吧?正确,这是您在将 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 从底部和顶部括起来尽可能接近的数字。请记住,我们仍然在 FPU 中以 80 位进行操作,因此虽然我显示一些位被移出,但它们仍然在 FPU reg 中,但在存储输出值时会在舍入操作期间被删除。

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

紧跟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

从下面:

  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

请记住,“四舍五入到最接近的偶数”中的 even 词仅在输入值介于括号值之间时才重要。只有这样词 even 才重要并“决定”应该选择哪个括号值。在上述情况下 even 无关紧要,我们必须简单地选择更接近的值,即2147483520

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

 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]
}

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

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

【讨论】:

  • 没有必要假设 OP 的编译器针对的是 387。针对现代 Intel 指令集的现代编译器将生成 cvttsd2si,它返回所使用的寄存器大小的最小值(32 位或 64 位)溢出。
  • @PascalCuoq:是的,你是对的。我的帖子太长了,无法添加更多内容。
猜你喜欢
  • 2013-01-11
  • 1970-01-01
  • 2017-06-18
  • 2017-12-08
  • 1970-01-01
  • 2021-03-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多