一个简单的浮点数序列化示例的问题

Ric*_*ick 2 c

我正在阅读教程http://beej.us/guide/bgnet/html/#serialization的序列化部分。

我正在审查将数字编码为可移植二进制形式的代码。

#include <stdint.h>

uint32_t htonf(float f)
{
    uint32_t p;
    uint32_t sign;

    if (f < 0) { sign = 1; f = -f; }
    else { sign = 0; }

    p = ((((uint32_t)f)&0x7fff)<<16) | (sign<<31); // whole part and sign
    p |= (uint32_t)(((f - (int)f) * 65536.0f))&0xffff; // fraction

    return p;
}

float ntohf(uint32_t p)
{
    float f = ((p>>16)&0x7fff); // whole part
    f += (p&0xffff) / 65536.0f; // fraction

    if (((p>>31)&0x1) == 0x1) { f = -f; } // sign bit set

    return f;
}
Run Code Online (Sandbox Code Playgroud)

我在这条线上遇到了问题p = ((((uint32_t)f)&0x7fff)<<16) | (sign<<31); // whole part and sign

根据原始代码注释,这一行提取整个部分符号,下一行处理分数部分。

然后我找到了一个关于如何float在内存中表示的图像并开始手动计算。

来自维基百科单精度浮点格式

所以我然后假设整个部分==指数部分

但如果基于上图,这(uint32_t)f)&0x7fff)<<16)将得到小数部分的最后一个。 15bits

现在我很困惑,我哪里做错了?

Ste*_*mit 6

重要的是要认识到这段代码不是什么。此代码不会对float值的各个位执行任何操作。(如果确实如此,它就不会像它声称的那样是可移植的和独立于机器的。)并且它创建的“可移植”字符串表示形式是固定的,而不是浮点。

\n

例如,如果我们使用此代码来转换数字-123.125,我们将得到二进制结果

\n
10000000011110110010000000000000\n
Run Code Online (Sandbox Code Playgroud)\n

或十六进制

\n
807b2000\n
Run Code Online (Sandbox Code Playgroud)\n

现在,那个数字在哪里10000000011110110010000000000000的?让我们把它分成符号、整数和小数部分:

\n
1 000000001111011 0010000000000000\n
Run Code Online (Sandbox Code Playgroud)\n

符号位是 1,因为我们原来的数字是负数。 000000001111011是 123 的 15 位二进制表示。并且0010000000000000是 8192。 8192 从哪里来?嗯,8192 \xc3\xb7 65536 是 0.125,这是我们的小数部分。(下面有更多相关内容。)

\n

代码是如何做到这一点的?让我们一步一步地看一下。

\n

(1)提取符号。很简单:这是普通的测试if(f < 0)

\n

(2)提取整数部分。这也很简单:我们获取浮点数f,并将其转换为类型unint32_t。当您在 C 中将浮点数转换为整数时,行为非常明显:它会丢弃小数部分并给出整数。所以如果f是 123.125,(uint32_t)f就是 123。

\n

(3)提取部分。由于我们已经得到了整数部分,因此我们可以通过从原始浮点数开始f并减去整数部分来分离分数。即 123.125 - 123 = 0.125。然后我们将小数部分乘以65536,即2 16

\n

为什么我们乘以 65536 而不是其他数字可能并不明显。从某种意义上说,您使用什么号码并不重要。这里的目标是获取一个小数f并将其转换为两个整数ab以便我们稍后可以再次恢复小数f(尽管可能是近似的)。我们恢复小数的方式f我们稍后

\n
a + b / x\n
Run Code Online (Sandbox Code Playgroud)\n

哪里x是其他数字。如果我们选择 1000 x,我们会将 123.125 分解为a123b和 125 的值。我们选择 65536 或 2 16,因为x这可以让我们最大限度地利用为小数部分分配的 16 位在我们的代表中。由于x是 65536,b因此必须是某个数字,我们可以除以 65536 以获得 0.125。因此b / 65536 = 0.125,通过简单的代数我们就有了b = 0.125 * 65536。合理?

\n

不管怎样,现在让我们看看执行步骤 1、2 和 3 的实际代码。

\n
if (f < 0) { sign = 1; f = -f; }\n
Run Code Online (Sandbox Code Playgroud)\n

十分简单。如果f是负数,我们的符号位将为 1,并且我们希望代码的其余部分对 的正数版本进行操作f

\n
p = ((((uint32_t)f)&0x7fff)<<16) | (sign<<31);\n
Run Code Online (Sandbox Code Playgroud)\n

如前所述,这里重要的部分是(uint32_t)f,它只获取 的整数(整数)部分f。位掩码& 0x7fff提取其中的低 15 位,丢弃其他任何内容。(这是因为我们的“可移植表示”只为整数部分分配 15 位,这意味着无法表示大于 2 15 -1 或 32767 的数字)。移位<< 16将其移动到最终unint32_t结果的上半部分,也就是它所属的位置。然后| (sign<<31)取出符号位并将其放入其所属的高位位置。

\n
p |= (uint32_t)(((f - (int)f) * 65536.0f))&0xffff; // fraction\n
Run Code Online (Sandbox Code Playgroud)\n

此处,(int)f重新计算 的整数(整数)部分f,然后f - (int)f提取分数。如上所述,我们将其乘以 65536。可能仍然有小数部分(即使是在乘法之后),所以我们(uint32_t)再次转换以丢弃它,只保留整数部分。我们只能处理 16 位小数,因此我们用 提取这些位(丢弃其他任何内容)& 0xffff,尽管这应该是不必要的,因为我们从小于 1 的正小数开始,并将其乘以 65536,所以我们最终应该得到小于 65536 的正数,即我们不应该有一个不完全适合 16 位的数字。最后,该p |=操作将我们刚刚计算的这 16 位填充到 的低位一半中p,我们就完成了。

\n
\n

附录:数字 65536 从何而来,以及为什么使用它而不是 10000 或其他数字,可能仍然不明显。因此,让我们回顾两个关键点:我们最终在这里处理整数。而且,从某种意义上说,65536这个数字实际上是相当随意的。

\n

归根结底,我们正在使用的任何位模式“实际上”只是一个整数。它不是一个字符,也不是一个浮点数,也不是一个指针 \xe2\x80\x94 它只是一个整数。如果它有N位,则表示0到2 N -1之间的整数。

\n

在我们这里使用的定点表示中,有三个子字段:1 位符号、15 位整数部分和 16 位小数部分。

\n

符号和整数部分的解释是显而易见的。但问题是:我们如何使用 16 位整数表示分数?

\n

答案是,我们选择一个数字,几乎任何数字来除以。我们可以将此数字称为“比例因子”。

\n

我们真的可以选择几乎任何数字。假设我选择数字 3467 作为缩放因子。现在我将几个不同的分数表示为整数:

\n

\xc2\xa0\xc2\xa0\xc2\xa0\xc2\xa0\xc2\xbd \xe2\x86\x92 1734/3467 \xe2\x86\x92 1734
\n\xc2\xa0\xc2\xa0\xc2\xa0 \xc2\xa0\xe2\x85\x93 \xe2\x86\x92 1155/3467 \xe2\x86\x92 1155
\n\xc2\xa0\xc2\xa0\xc2\xa0\xc2\xa00.125 \xe2\x86 \x92 433/3467 \xe2\x86\x92 433

\n

所以我的分数 \xc2\xbd、\xe2\x85\x93 和 0.125 由整数 1734、1155 和 433 表示。为了恢复我的原始分数,我只需除以 3467:

\n

\xc2\xa0\xc2\xa0\xc2\xa0\xc2\xa01734 \xe2\x86\x92 1734 \xc3\xb7 3467 \xe2\x86\x92 0.500144
\n\xc2\xa0\xc2\xa0\xc2\xa0\ xc2\xa01155 \xe2\x86\x92 1155 \xc3\xb7 3467 \xe2\x86\x92 0.333141
\n\xc2\xa0\xc2\xa0\xc2\xa0\xc2\xa0433 \xe2\x86\x92 1734 \xc3\ xb7 3467 \xe2\x86\x92 0.124891

\n

显然我无法准确地恢复我的原始分数,但我已经非常接近了。

\n

另一件令人好奇的事情是,3467这个数字“住在”哪里?如果您只查看数字 1734、1155 和 433,您怎么知道应该将它们除以 3467?答案是,你不知道,至少,不只是通过观察它们。3567 必须是我愚蠢的小数格式定义的一部分;人们只需要知道,因为我这么说过,他们在构造整数来表示分数时必须乘以 3467,并在恢复原始分数时除以 3467。

\n

另一件需要考虑的事情是选择各种不同的缩放因子的影响。首先,由于最终我们将使用 16 位整数来表示小数,因此我们绝对不能使用大于 65536 的缩放因子。如果这样做,有时我们会结束大于 65535 的整数,它不适合 16 位。例如,假设我们尝试使用比例因子 70000,并假设我们尝试表示分数 0.95。现在,0.95 等于 66500/70000,所以我们的整数将是 66500,但这不适合 16 位。

\n

另一方面,事实证明,理想情况下我们也不想使用小于65536 的数字。我们使用的数字越小,我们浪费的 16 位小数表示就越多。当我早些时候在愚蠢的示例中选择 3467 时,这意味着我将表示从 0/3467 = 0.00000 和 1/3467 = 0.000288 到 3466/3467 = 0.999711 的分数。但我永远不会使用 3467 到 65536 之间的任何整数。它们会被浪费,并且如果不使用它们,我会不必要地限制我可以表示的分数的精度。

\n

使用的“最佳”(最不浪费)缩放因子是 65536,尽管还有另一个考虑因素,即您希望能够准确表示哪些分数?当我使用 3467 作为缩放因子时,我无法准确表示任何测试数字 \xc2\xbd、\xe2\x85\x93 或 0.125。如果我们使用 65536 作为缩放因子,结果表明我们可以表示涉及 2 的小幂 \xe2\x80\x94 的分数,即二分之一、四分之一、八分之一、十六分之一等。 \xe2\x80\x94 但不能任何其他分数,特别是不是大多数小数分数,例如 0.1。如果我们希望能够准确地表示小数,我们就必须使用 10 的幂的缩放因子。 16 位可以容纳的最大 10 的幂是 10000,这确实可以让我们准确地表示十进制小数为 0.00001,尽管我们会浪费大约 5/6(或 85%)的 16 位小数范围。

\n

因此,如果我们想要准确地表示小数,而不浪费精度,那么不可避免的结论是,我们一开始就不应该为小数字段分配 16 位。更好的选择是 10 位(理想缩放因子 1024,我们使用 1000,仅浪费 2%)或 20 位(理想缩放因子 1048576,我们使用 1000000,浪费约 5%)。

\n