ldexp 和 frexp 正数分解。
如果您可以接受最多 2-16 的相对误差,则可以仅使用基本算术和 ld/frexp 分解来表示转换的两边。
请注意,这比 struct hack 慢得多,后者可以更简洁地表示为 struct.unpack('f', struct.pack('I', value))。
这里是分解方法。
def to_bits(x):
man, exp = math.frexp(x)
return int((2 * man + (exp + 125)) * 0x800000)
def from_bits(y):
y -= 0x3e800000
return math.ldexp(
float(0x800000 + y & 0x7fffff) / 0x1000000,
(y - 0x800000) >> 23)
虽然from_bits 函数看起来更吓人,但实际上它只不过是to_bits 的倒数,修改后我们只执行单次浮点除法(不是出于速度考虑,只是因为它应该是排序当我们确实需要使用浮点数的机器表示时,我们的心态)。因此,我将重点解释正向变换。
推导
回想一下,(正)IEEE 754 浮点数表示为有偏指数及其尾数的元组。低 23 位 m 是尾数,高 8 位 e(减去最高有效位,我们假设它始终为零)表示指数,因此
x = (1 + m / 223) * 2e - 127
令 man' = m / 223 和 exp' = e - 127,然后 0 man' exp' 是一个整数。因此
(man' + exp' + 127) * 223
给出 IEEE 754 表示。
另一方面,frexp 分解计算出一对 man, exp = frexp(x),使得 man * 2exp = x,并且0.5 人
稍加思考就会发现 man' = 2 * man - 1 和 exp' = exp - 1,因此它的 IEEE 机器表示是
(man' + exp' + 127) * 0x800000 = (2 * man + exp + 125) * 0x800000
错误分析
我们期望有多少舍入误差?好吧,让我们假设frexp 在其分解中没有引入任何错误。不幸的是,这是不可能的,但我们可以放松一下。
主要特点是计算2 * man + (exp + 125)。为什么? 0x800000 是 2 的完美幂,因此 2 的幂的浮点乘法几乎总是无损的(除非我们溢出),因为 FPU 只是将 23 << 23 添加到其机器表示中(不触及尾数,这是出现错误的时候)。同样,乘法 2 * man 也是无损的(类似于将 1 << 23 添加到机器表示中)。此外,exp 和 125 是整数,因此 (exp + 125) 也可以精确计算。
因此,我们要分析m + e 的错误行为,其中1 m m 已填充所有 23 位(对应于 m = 2 - 2-22)和 e = +/- 127。在这里,不幸的是,这个添加将破坏 m 的 8 个最低有效位,因为它必须将 m(其指数范围为 20)重新归一化为指数范围 28,这意味着丢失 8 位。然而,由于尾数有 24 个有效位,我们实际上损失了 2-(24 - 8) 量的精度,这是错误的上限。
在 from_bits 的类似推理中,您可以证明 float(0x800000 + y & 0x7fffff) 基本上是在计算操作 (1.0f + m),其中 m 可能具有高达 23 位的精度,并且严格来说要少比 1。因此,我们在 20 的范围内添加一个精确的数字,并在 2-1 的范围内添加另一个数字,因此我们预计会损失一个少量。这表明我们将在反向转换中产生高达 2-22 的相对误差。
这两种转换都只需要很少的舍入,如果你在 to_bits 中加上一个额外的乘法,你也可以将它的误差降低到只有 2-22。
结束语
不要在生产中这样做。
- 您永远不必显式操作数字的机器表示。
- 即使出于某些不虔诚的原因您需要这样做,您也不应该做这种 hacky 的事情。
这只是一个看起来很有趣的巧妙浮动破解。它的意义不止于此。