0

(对不起,我想出了一些有趣的想法......请耐心等待......)

假设我有一个“双”值,包括:

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

如果我是对的,代表 1234.6565。

我希望能够以位的形式分别访问符号、指数、隐式和尾数字段!并使用AND、OR、XOR 等按位运算或字符串运算(如“left”、mid 等)来操作它们。

然后我想从被操纵的位中拼出一个新的替身。

例如,将符号位设置为 1 将使数字变为负数,向/从指数添加或减去 1 将使值加倍/减半,剥离指数的重新计算(无偏)值指示的位置后面的所有位将转换值为整数等等。

其他任务将/可能是找到最后设置的位,计算它对值的贡献程度,检查最后一位是“1”(二进制“奇数”)还是“0”(二进制“偶数”)等.

我在程序中看到过类似的,只是在运行中找不到它。我可能记得“重新解释演员表”或类似的东西?我认为有一些图书馆或工具包或“howtos”可以提供对此类的访问,并希望这里有读者可以指出我的此类内容。

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

起点是我可以寻址为“x”的任何双精度值,

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

如何做简单,并让它以良好的性能工作?

4

1 回答 1

0

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

第 1 步是访问 afloat或的各个位double。有很多方法可以做到这一点,但最常见的是使用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 是显式尾数位的数量)。也就是说,我们的十六进制尾数(恢复了前 1 位)实际上应该更像. 我们可以通过调整指数减去 52 来解决这个问题:0x134aa04189374c0x1.34aa04189374c

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

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

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

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

另一方面,如果指数字段具有最大值(即 2047 或 2 11 -1,对于双精度数),则表示一个特殊标记。在这种情况下,如果尾数为 0,则我们有一个无穷大,符号位区分正无穷大和负无穷大。或者,如果指数为最大值且尾数不为 0,则我们有一个“非数字”标记,或者NaNNaN尾数中的特定非零值可用于区分不同类型的那个小细节。

(如果您不熟悉无穷大和 NaN,它们就是 IEEE-754 所说的,当正确的数学结果不是普通数字时,某些操作应该返回。例如,sqrt(-1.0)返回NaN1./0.通常给出inf。有一整套关于无穷大和 NaN 的 IEEE-754 规则,例如atan(inf)返回 π/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 添加到次正规数的指数,反映了以下事实,即次正规数是“用最小允许指数的值解释的,该指数大一”(根据维基百科关于次正规数的文章)。

我说这都是“直截了当,如果有点深奥”,但正如您所见,虽然提取原始尾数和指数值确实非常简单,但解释它们的实际含义可能是一个挑战!


如果您已经有了原始指数和尾数,那么从另一个方向返回——即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),然后使用我们迄今为止讨论过的技术。)


您最初的问题是关于“按位拆分”,这是我们一直在讨论的。但值得注意的是,如果您不想乱用原始位,并且不想/不需要假设您的机器使用 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 × 2 11 = 1234.6565(大约)。(它与我们的按位分解相比如何?嗯,我们的尾数是0x34aa04189374c, 或0x1.34aa04189374c,十进制是 1.20571923828125,这是ldexp刚刚给我们的尾数的两倍。但我们的指数是 1033 - 1023 = 10,少了一个,所以它在洗涤中出来:1.20571923828125 × 2 10 = 0.602859619140625 × 2 11 = 1234.6565。)

还有一个功能ldexp朝另一个方向发展:

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

这再次打印1234.656500


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

于 2021-09-16T11:49:55.763 回答