【问题标题】:Portable way to serialize float as 32-bit integer将浮点序列化为 32 位整数的便携式方法
【发布时间】:2016-11-04 06:35:42
【问题描述】:

我一直在努力寻找一种可移植的方法来序列化 C 和 C++ 中的 32 位浮点变量,以便与微控制器之间发送和接收。我希望格式定义得足够好,以便序列化/反序列化也可以从其他语言完成,而无需太多努力。相关问题是:

Portability of binary serialization of double/float type in C++

Serialize double and float with C

c++ portable conversion of long to double

我知道在大多数情况下,typecast union/memcpy 会很好地工作,因为浮点表示是相同的,但我希望有更多的控制和头脑。到目前为止,我想出的是以下内容:

void serialize_float32(uint8_t* buffer, float number, int32_t *index) {
    int e = 0;
    float sig = frexpf(number, &e);
    float sig_abs = fabsf(sig);
    uint32_t sig_i = 0;

    if (sig_abs >= 0.5) {
        sig_i = (uint32_t)((sig_abs - 0.5f) * 2.0f * 8388608.0f);
        e += 126;
    }

    uint32_t res = ((e & 0xFF) << 23) | (sig_i & 0x7FFFFF);
    if (sig < 0) {
        res |= 1 << 31;
    }

    buffer[(*index)++] = (res >> 24) & 0xFF;
    buffer[(*index)++] = (res >> 16) & 0xFF;
    buffer[(*index)++] = (res >> 8) & 0xFF;
    buffer[(*index)++] = res & 0xFF;
}

float deserialize_float32(const uint8_t *buffer, int32_t *index) {
    uint32_t res = ((uint32_t) buffer[*index]) << 24 |
                ((uint32_t) buffer[*index + 1]) << 16 |
                ((uint32_t) buffer[*index + 2]) << 8 |
                ((uint32_t) buffer[*index + 3]);
    *index += 4;

    int e = (res >> 23) & 0xFF;
    uint32_t sig_i = res & 0x7FFFFF;
    bool neg = res & (1 << 31);

    float sig = 0.0;
    if (e != 0 || sig_i != 0) {
        sig = (float)sig_i / (8388608.0 * 2.0) + 0.5;
        e -= 126;
    }

    if (neg) {
        sig = -sig;
    }

    return ldexpf(sig, e);
}

frexpldexp 函数似乎是为此目的而设计的,但如果它们不可用,我也尝试使用常用函数手动实现它们:

float frexpf_slow(float f, int *e) {
    if (f == 0.0) {
        *e = 0;
        return 0.0;
    }

    *e = ceil(log2f(fabsf(f)));
    float res = f / powf(2.0, (float)*e);

    // Make sure that the magnitude stays below 1 so that no overflow occurs
    // during serialization. This seems to be required after doing some manual
    // testing.

    if (res >= 1.0) {
        res -= 0.5;
        *e += 1;
    }

    if (res <= -1.0) {
        res += 0.5;
        *e += 1;
    }

    return res;
}

float ldexpf_slow(float f, int e) {
    return f * powf(2.0, (float)e);
}

我一直在考虑的一件事是使用 8388608 (2^23) 还是 8388607 (2^23 - 1) 作为乘数。文档说 frexp 返回的值的幅度小于 1,经过一些实验后,似乎 8388608 给出的结果与实际浮点数是位精确的,我找不到任何溢出的极端情况。但是,使用不同的编译器/系统可能并非如此。如果这会成为一个问题,那么一个较小的乘数会降低一点精度,这对我来说也很好。我知道这不能处理 Inf 或 NaN,但现在这不是必需的。

所以,最后,我的问题是:这看起来是一种合理的方法,还是我只是在制作一个仍然存在可移植性问题的复杂解决方案?

【问题讨论】:

  • 简短回答:你不能真正以可移植的方式做到这一点,除非使用反序列化库/工具,例如 google protobuf。
  • 那么我提出的方法有什么问题?我通常读到的问题是浮点表示不能保证在所有系统上都是相同的,所以我的尝试旨在生成始终相同的东西,无论浮点的内部表示是什么。
  • Endianess 列举其中一个问题。
  • 据我所知,字节移位是安全的:stackoverflow.com/questions/7184789/…我没有使用任何类型转换。
  • Frexp 永远不会返回 1,所以不用担心。

标签: c++ c floating-point embedded


【解决方案1】:

假设浮点数是 IEEE 754 格式,提取尾数、指数和符号是完全可移植的:

uint32_t internal;
float value = //...some value
memcpy( &internal , &value , sizeof( value ) );

const uint32_t sign =     ( internal >> 31u ) & 0x1u;
const uint32_t mantissa = ( internal >> 0u  ) & 0x7FFFFFu;
const uint32_t exponent = ( internal >> 23u ) & 0xFFu;

反转构造浮点数的过程。

如果您只想发送整个浮点数,只需将其复制到缓冲区即可。即使 float 不是 IEEE 754,这也可以工作,但它必须是 32 位并且整数和浮点类型的字节序必须相同:

buffer[0] = ( internal >> 0u  ) & 0xFFu;
buffer[1] = ( internal >> 8u  ) & 0xFFu;
buffer[2] = ( internal >> 16u ) & 0xFFu;
buffer[3] = ( internal >> 24u ) & 0xFFu;

【讨论】:

  • 如果我假设我根本不需要提取它们,那么我可以立即进行类型转换。
  • @BenjaminVedder 你是什么意思?
  • 那么我可以这样做:uint32_t internal; float value = //...一些值 memcpy( &internal , &value , sizeof( value ) );缓冲区[(*index)++] = (内部 >> 24) & 0xFF;缓冲区[(*index)++] = (内部 >> 16) & 0xFF;缓冲区[(*index)++] = (内部 >> 8) & 0xFF;缓冲区[(*index)++] = 内部 & 0xFF;使它变得如此复杂的全部意义在于我希望能够处理具有非标准浮点表示的情况,但实际上这在 2016 年可能不是问题。 (编辑:对不起格式,评论似乎不支持换行)
  • @2501:你能用 C 标准的引用来支持你的主张吗?我怀疑您是否可以假设 utin32_tfloat 的字节顺序与您似乎暗示的相同。
  • @chqrlie 标准不知道字节序是什么,这是硬件的产物。我认为不存在支持 754 并且可以同时处理不同寄存器类型的小端和大端的现代机器。
【解决方案2】:

serialize_float 中似乎有错误:最后 4 行应为:

buffer[(*index)++] = (res >> 24) & 0xFF;
buffer[(*index)++] = (res >> 16) & 0xFF;
buffer[(*index)++] = (res >> 8) & 0xFF;
buffer[(*index)++] = res & 0xFF;

由于126 而不是128 的偏移,您的方法可能不适用于无穷大和/或NaN。请注意,您可以通过广泛的测试来验证它:只有 40 亿个值,尝试所有可能性应该不会花费很长时间。

float 值在内存中的实际表示可能在不同的架构上有所不同,但 IEEE 854(或更准确地说是 IEC 60559)在当今非常普遍。您可以通过检查是否定义了__STDC_IEC_559__ 来验证您的特定目标是否符合要求。但是请注意,即使您可以假设 IEEE 854,您也必须处理系统之间可能存在的不同字节序。您不能假设 floats 的字节序与同一平台的整数的字节序相同。

还要注意,简单的转换是不正确的:uint32_t res = *(uint32_t *)&amp;number; 违反了严格的别名规则。您应该使用union 或使用memcpy(&amp;res, &amp;number, sizeof(res));

【讨论】:

  • 谢谢!这是一个复制粘贴错误。实际上,我有一个额外的功能可以做到这一点,但是对于这个问题,我把它放在同一个地方,以便更容易看到发生了什么。
  • 关于循环的要点!实际上,除了 Inf 和 NaN 之外,我整晚都这样做了,而且在我的笔记本电脑上,它似乎适用于所有值。不过我不知道其他系统。
  • 移位的全部意义在于它完全避免了字节序。
  • @2501:当然。我只是告诉 OP,如果他使用更简单的序列化方法,如果他可以假设 float 表示为 IEEE 854,则必须考虑字节顺序。
  • 嗯,也许这是 C 和 C++ 标准之间的差异。 C++ 标准 9.3 状态 [ Note: One special guarantee is made in order to simplify the use of unions: If a standard-layout union contains several standard-layout structs that share a common initial sequence (9.2), and if a non-static data member of an object of this standard-layout union type is active and is one of the standard-layout structs, it is permitted to inspect the common initial sequence of any of the standard-layout struct members; see 9.2. — end note ]。在这种情况下,没有共同的初始序列。
猜你喜欢
  • 2011-01-28
  • 2011-10-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多