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
显然67108872 比67108864 更接近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 存储到内存的指令。无论您在程序中如何声明它,一切都只是被视为已签名的位模式。
很长,但希望有人能从中学到一些东西。