Pascal的答案没问题,但缺乏细节,这导致一些用户理解困难;-)。如果你对低级别的情况感兴趣(假设协处理器而不是软件处理浮点运算),请继续阅读。
在32位浮点数(IEEE 754)中,您可以存储范围在 [-224 ... 224] 内的所有整数。 超出此范围的整数也可能具有精确表示为浮点数,但并非所有整数都具有。问题在于,在浮点数中,您只能使用24个有效位。
以下是从int->float转换通常在低级别上看起来如何:
fild dword ptr[your int]
fstp dword ptr[your float]
这只是2个协处理器指令的序列。第一个将32位整数加载到协处理器的堆栈上,并将其转换为80位宽的浮点数。
Intel® 64 和 IA-32 架构软件开发人员手册:
(使用 X87 FPU 进行编程):
当从内存中载入浮点、整数或打包BCD整数值到任何 x87 FPU 数据寄存器时,这些值会自动转换为双扩展精度浮点格式(如果它们还不是该格式)。
由于FPU寄存器是80位宽的浮点数 - 在这里使用 fild
没有问题,因为32位整数完全适合浮点格式的64位尾数。
到了第二部分 - fstp
就有点棘手并且可能令人惊讶。它应该将80位浮点数存储在32位浮点数中。虽然该问题涉及整数值,但协处理器实际上可能执行 '舍入'。怎么可能会对整数值进行舍入,即使它存储在浮点格式中呢?;-)。
我简单地解释一下 - 让我们先看看 x87 提供了哪些舍入模式(它们是IEEE 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
};
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
}
}
好的,不错,但这和整数值有什么关系呢?耐心一点……要理解为什么在将整数转换为浮点数时需要涉及舍入模式,请先看最明显的方法——截断(非默认)的整数到浮点数的转换方式,可能是这样的:
- 记录符号
- 如果小于零,则取反整数
- 找到最左边的1的位置
- 将整数向右/左移位,使得上面找到的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个位置。在此过程中,您会失去(它们不适合32位浮点格式)从右边截取的7个lsb位。它们是:
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
更好,对吧?是正确的。这也是你可能想要讨论将整数转换为32位浮点数时舍入的位置。
现在让我们看看默认的“最近偶数舍入”模式是如何工作的,以及它对OP案例的影响。再考虑同一个例子。
67108871 = 00000100_00000000_00000000_00000111
我们知道,我们需要进行3次右移来将最左边的1位放到第23位:
00000000_1.[0000000_00000000_00000000] 111 * 2^26 (3 bits shifted out)
'四舍六入五成双'的过程涉及查找两个数字,它们从下面和上面尽可能接近地包围输入值67108871
。请记住,我们仍然在80位FPU中运行,因此尽管我展示了一些位被移出,但它们仍然在FPU寄存器中,但在存储输出值时将在舍入操作中删除。
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
显然,67108872
比67108864
更接近于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
请记住,“四舍五入到最近偶数”中的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]
}
尽管测试值看起来应该被视为无符号值,但由于没有将带符号和无符号值加载到FPU的单独指令,它将被加载为-2
31 。 同样,您将找不到允许您从FPU存储无符号值到内存的指令。 不管您在程序中如何声明,一切都只是被视为有符号的位模式。长话短说,这个问题就是缺少相应的指令。希望这篇文章能对大家有所启示。