按位拆分 IEEE 754 double 的尾数?如何访问位结构,

bitwise splitting the mantissa of a IEEE 754 double? how to access bit structure,

(抱歉,我想出了一些有趣的点子...请耐心等待...)

假设我有一个 'double' 值,包括:

                 implicit
sign exponent    bit         mantissa
0    10000001001 (1).0011010010101010000001000001100010010011011101001100

代表 1234.6565 如果我是对的。

我希望能够以位的形式分别访问符号、指数、隐式和尾数字段!,并使用 按位运算 对它们进行操作,例如 AND、OR、 XOR ... 或 字符串运算 如 'left'、mid 等

然后我想从被操纵的位中拼出一个新的双精度数。

例如将符号位设置为 1 将使数字为负,加或减 1 to/from 指数将 double/halve 该值,去除指数的重新计算(无偏)值指示的位置后面的所有位将将值转换为整数等。

其他任务would/could是找到最后设置的位,计算它对值的贡献,检查最后一位是'1'(二进制'odd')还是'0' (二进制'even')等。

我在程序中看到过类似的,只是找不到。我可能记得 'reinterpret cast' 或类似的东西?我认为有图书馆或工具包或 'howtos' 可以提供对此类的访问,并希望这里的读者可以向我指出此类内容。

我想要一个接近简单处理器指令和简单 C 代码的解决方案。我在 Debian Linux 中工作并使用默认情况下的 gcc 进行编译。

startpoint 是我可以作为 'x',

寻址的任何双精度值

起点2是我不是!一位经验丰富的程序员:-(

如何既简单又高效地工作?

虽然有点深奥,但很简单。

第 1 步是访问 floatdouble 的各个位。有多种方法可以做到这一点,但最常见的是使用 char * 指针或联合。为了我们今天的目的,让我们使用联合。 [这个选择有一些微妙之处,我将在脚注中加以说明。]

union doublebits {
    double d;
    uint64_t bits;
};

union doublebits x;
x.d = 1234.6565;

现在 x.bits 让我们可以访问 double 值的位和字节作为 64 位无符号整数。首先,我们可以将它们打印出来:

printf("bits: %llx\n", x.bits);

这会打印

bits: 40934aa04189374c

我们正在路上。

剩下的就是“简单的”位操作。 我们将从蛮力、显而易见的方式开始:

int sign = x.bits >> 63;
int exponent = (x.bits >> 52) & 0x7ff;
long long mantissa = x.bits & 0xfffffffffffff;

printf("sign = %d, exponent = %d, mantissa = %llx\n", sign, exponent, mantissa);

这会打印

sign = 0, exponent = 1033, mantissa = 34aa04189374c

并且这些值与您在问题中显示的位分解完全匹配,因此看起来您对数字 1234.6565 的判断是正确的。

到目前为止,我们拥有的是原始指数和尾数值。 如您所知,指数是偏移的,尾数有一个隐含的前导“1”,所以让我们处理这些:

exponent -= 1023;
mantissa |= 1ULL << 52;

(实际上这不太正确。很快我们将不得不解决一些与非规范化数字、无穷大和 NaN 相关的额外问题。)

现在我们有了真正的尾数和指数,我们可以做一些数学运算来重新组合它们,看看是否一切正常:

double check = (double)mantissa * pow(2, exponent);

但是如果你尝试这样做,它会给出错误的答案,这是因为对我来说,这始终是最难的部分:尾数中的小数点在哪里,真的吗? (实际上,它不是一个“小数点”,反正,因为我们不是在十进制工作。形式上它是一个“小数点”,但听起来太闷了,所以我打算继续使用“小数点”,即使虽然这是错误的。向任何以错误方式摩擦的学究致歉。)

当我们做 mantissa * pow(2, exponent) 时,我们假设一个小数点,实际上,在尾数的 右边 末尾,但实际上,它应该是 52 位到左边(当然,数字 52 是显式尾数位数)。也就是说,我们的十六进制尾数 0x134aa04189374c(恢复前导 1 位)实际上应该更像 0x1.34aa04189374c。我们可以通过调整指数来解决这个问题,减去 52:

double check = (double)mantissa * pow(2, exponent - 52);
printf("check = %f\n", check);

所以现在 check 是 1234.6565(加上或减去一些舍入误差)。这与我们开始时使用的数字相同,所以看起来我们的提取在所有方面都是正确的。

但我们还有一些未完成的工作,因为对于完全通用的解决方案,我们必须处理“次正规”(也称为“非正规化”)数字,以及特殊表示 infNaN.

这些皱纹是由指数场控制的。如果指数(在减去偏差之前)恰好为 0,则表示一个 次正规 数字,即其尾数不在(十进制)1.00000 到 1.99999 的正常范围内的数字。次正规数 not 具有隐含的前导“1”位,尾数最终在 0.00000 到 0.99999 的范围内。 (这也最终成为必须表示普通数字 0.0 的方式,因为它显然不能有隐含的前导“1”位!)

另一方面,如果指数字段有其最大值值(即2047,或211-1,对于双)这表示一个特殊的标记。在那种情况下,如果尾数为 0,则我们有一个无穷大,符号位区分正无穷大和负无穷大。或者,如果指数为最大值且尾数不为 0,则我们有一个“非数字”标记,即 NaN。尾数中的特定非零值可用于区分不同类型的 NaN,如“安静”和“信号”,尽管事实证明可能用于此的特定值不是标准的,所以我们会忽略那个小细节。

(如果您不熟悉无穷大和 NaN,它们就是 IEEE-754 所说的,当正确的数学结果不是普通数字时,某些操作应该 return。例如,sqrt(-1.0) returns NaN,而 1./0. 通常给出 inf。有一整套关于无穷大和 NaN 的 IEEE-754 规则,例如atan(inf) returns π/2.)

最重要的是,我们必须首先检查指数值,而不是盲目地追加隐含的 1 位,并根据指数是否具有最大值(表示特殊)来做一些不同的事情,一个中间值(表示普通数),或 0(表示次正规数):

if(exponent == 2047) {
    /* inf or NAN */
    if(mantissa != 0)
         printf("NaN\n");
    else if(sign)
         printf("-inf\n");
    else printf("inf\n");
} else if(exponent != 0) {
    /* ordinary value */
    mantissa |= 1ULL << 52;
} else {
    /* subnormal */
    exponent++;
}

exponent -= 1023;

最后一次调整,将次正规数的指数加 1,反映了次正规数“用最小允许指数的值解释,即大一”(根据 [=52= 上的维基百科文章) ]).

我说过这一切都是“直截了当,如果有点深奥”,但正如你所看到的,虽然提取原始尾数和指数值确实非常简单,解释它们实际上是什么 mean 可以是一个挑战!


如果您已经有了原始指数和尾数,那么从另一个方向返回——即从它们构造一个 double 值——同样简单:

sign = 1;
exponent = 1024;
mantissa = 0x921fb54442d18;

x.bits = ((uint64_t)sign << 63) | ((uint64_t)exponent << 52) | mantissa;

printf("%.15f\n", x.d);

这个答案太长了,所以现在我不打算深入研究如何从头开始为任意实数构造适当的指数和尾数的问题。 (我,我通常做等同于 x.d = atof(the number I care about),然后使用我们目前讨论过的技术。)


您最初的问题是关于“按位拆分”的,这正是我们一直在讨论的内容。但值得注意的是,如果您不想处理原始位,并且您不 want/need 假设您的机器使用 IEEE-754,那么有一种更便携的方法可以完成所有这些工作。如果只想将浮点数拆分为尾数和指数,可以使用标准库frexp函数:

int exp;
double mant = frexp(1234.6565, &exp);
printf("mant = %.15f, exp = %d\n", mant, exp);

这会打印

mant = 0.602859619140625, exp = 11

这看起来是对的,因为 0.602859619140625 × 211 = 1234.6565(大约)。 (它与我们的按位分解相比如何?嗯,我们的尾数是 0x34aa04189374c,或 0x1.34aa04189374c,十进制为 1.20571923828125,是 ldexp 刚给我们的尾数的两倍。但是我们的指数是 1033 - 1023 = 10,少了一个,所以结果很简单:1.20571923828125 × 210 = 0.602859619140625 × 211 = 1234.6565.)

还有一个函数 ldexp 朝另一个方向发展:

double x2 = ldexp(mant, exp);
printf("%f\n", x2);

这将再次打印 1234.656500


脚注:当您尝试访问某些东西的原始位时,当然我们一直在这里做,有一些潜在的可移植性和正确性问题与称为 strict aliasing 的东西有关。严格来说,根据您询问的对象,您可能需要使用 unsigned char 数组作为联合的另一部分,而不是像我在这里所做的那样使用 uint64_t。还有人说你根本不能便携地使用联合,你必须使用 memcpy 将字节复制到一个完全独立的数据结构中,尽管我认为他们正在考虑 C++,而不是C.