按位分割IEEE 754双精度数的尾数?如何访问位结构,

人气:209 发布:2022-10-16 标签: double bit-manipulation ieee-754 c mantissa

问题描述

(对不起,我想出了一些有趣的主意...请原谅我...)

假设我有一个‘Double’值,包括:

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

如果我是对的,则代表1234.6565。

我希望能够以位!的形式分别访问符号、指数、隐式和尾数字段,并使用逐位操作,如AND、OR、XOR...或字符串操作,如‘Left’、MID等。

然后我想从被操纵的比特中猜出一个新的双人比特。

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

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

我在程序中也看到过类似的东西,只是在运行中找不到。我可能还记得《重新诠释演员阵容》之类的东西?我认为周围有一些图书馆、工具包或‘HOWTO’可以提供对这些内容的访问,希望在座的读者能给我指点一下。

我想要一个接近简单的处理器指令和简单的C代码的解决方案。我在Debian Linux上工作,和GCC一起编译,这是默认的。

起始点是我可以称为‘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
", 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
", sign, exponent, mantissa);

此打印

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

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

到目前为止,我们得到的是原始指数和尾数值。 如您所知,指数是偏移量,尾数具有隐式前导&1&q;,因此让我们来处理这些:

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
", check);
因此,现在check是1234.6565(加上或减去一些舍入误差)。这是我们开始时的相同数字,所以看起来我们的提取在所有方面都是正确的。

但我们还有一些未完成的工作,因为对于完全通用的解决方案,我们必须处理infNaN的特殊表示。

这些褶皱由指数场控制。如果指数(减去偏置之前)正好是0,则表示低于正常的数字,即尾数不在(十进制)1.00000到1.99999的正常范围内的数字。低于正态的数字不具有隐式前导1和尾数位,尾数的范围为0.00000到0.99999。(这也是表示普通数字0.0的方式,因为它显然不能有隐式的前导1位!)

另一方面,如果指数字段有其最大值(即,2047,或211-1,对于双精度型,则表示特殊标记)。在这种情况下,如果尾数是0,我们就有一个无穷大,符号位区分正无穷大和负无穷大。或者,如果指数是max,尾数不是0,那么我们有一个&Quot;而不是一个数字&Quot;标记,或NaN。尾数中的特定非零值可用于区分不同类型的NaN,如&Quot;Quite;和";Signals&Quot;One,尽管事实证明可能用于此目的的特定值不是标准的,因此我们将忽略该小细节。

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

底线是,我们不能只盲目地添加隐含的1位,我们必须首先检查指数值,并根据指数是否有最大值(表示特殊)、中间值(表示普通数字)或0(表示不正常的数字):

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

exponent -= 1023;
最后一次调整是将低于正态数的指数加1,这反映了这样一个事实,即用最小允许指数的值来解释次正态。(根据subnormal numbers上的维基百科文章)。

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

如果您已经有了原始指数和尾数,反向返回-即根据它们构造double值-也同样简单:

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

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

printf("%.15f
", 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
", 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
", x2);

这将再次打印1234.656500

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

595