繁体   English   中英

如何从字节缓冲区读取 little-endian 64 位值?

[英]How do I read a little-endian 64-bit value from a byte buffer?

在 C 应用程序(不是 C++)中,我有一个通过 .network 接收的数据字节数组。 该数组长 9 个字节。 字节 1 到 8(从零开始)表示 64 位 integer 值作为小端。 我的 CPU 也使用小端。

如何将这些字节从数组转换为 integer 数字?

我试过这个:

uint8_t rx_buffer[2000];
//recvfrom(sock, rx_buffer, sizeof(rx_buffer) - 1, ...)
int64_t sender_time_us = *(rx_buffer + 1);

但它给了我 89、219、234 或 27 之类的值。发件人将这些值视为 1647719702937548、1647719733002117 或 1647719743790424。(这些示例不匹配,它们只是随机样本。)

不安全的解决方案:

int64_t sender_time_us = *(int64_t*)(rx_buffer + 1);

这可能是 alignment 违规,并且是严格的别名规则违规 这是未定义的行为。 在某些机器上,这可能会因总线错误而终止您的程序。


安全解决方案:

int64_t sender_time_us;
memcpy( &sender_time_us, rx_buffer + 1, sizeof( int64_t ) );

@Nate Eldredge 指出虽然这个解决方案可能看起来效率低下,但一个体面的编译器应该将其优化为高效的东西。 .net 效果将是 (a) 强制编译器正确处理未对齐的访问,如果目标需要任何特殊处理,(b) 让编译器正确理解别名并防止任何会破坏它的优化。 对于能够正常处理未对齐访问的目标,生成的代码可能根本不会改变。

您的代码仅获得一个uint8_t 您需要先转换为int64_t 是这样的:

int64_t* pBuffer = (int64_t*)(rx_buffer + 1);
int64_t sender_time_us = *pBuffer;

但是您应该知道,某些 CPU 可能不喜欢访问未对齐的 64 位值。 如果您知道字节顺序,这也可能没问题,但实际上以更便携的方式处理它会更好。

读取 little-endian 64 位值的可移植方法非常简单:

inline static uint64_t load_u64le(const void *p) {
    const unsigned char *q = p;
    uint64_t result = 0;
    result |= q[7]; result <<= 8;
    result |= q[6]; result <<= 8;
    result |= q[5]; result <<= 8;
    result |= q[4]; result <<= 8;
    result |= q[3]; result <<= 8;
    result |= q[2]; result <<= 8;
    result |= q[1]; result <<= 8;
    result |= q[0];
    return result;
}

inline static int64_t load_i64le(const void *p) {
    return (int64_t)load_u64le(p);
}

只需将此助手 function 调用为read_i64le(rx_buffer + 1) 现代编译器能够在可能的情况下将其优化为架构上的单个指令

要读取一个 64 位值,其中您明确知道字节顺序与本机 ABI 一致,您可以使用以下命令:

inline static uint64_t load_u64(const void *p) {
    uint64_t result;
    memcpy(&result, p, sizeof(result));
    return result;
}

它有更好的机会被优化为一个简单的加载,假设只假设编译器将一个短的memcpy优化为一个内联 memory 加载。

为了获得最佳效果,您可以使用:

inline static uint64_t load_u64le(const void *p) {
    uint64_t result = 0;
#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
    memcpy(&result, p, sizeof(result));
#else
    const unsigned char *q = p;
    result |= q[7]; result <<= 8;
    result |= q[6]; result <<= 8;
    result |= q[5]; result <<= 8;
    result |= q[4]; result <<= 8;
    result |= q[3]; result <<= 8;
    result |= q[2]; result <<= 8;
    result |= q[1]; result <<= 8;
    result |= q[0];
#endif
    return result;
}

现在,为什么你不应该像其他答案建议的那样转换偏移指针:首先,因为取消引用未对齐的指针是 UB。 并非每个体系结构都支持从任意地址读取大于 8 位的字,即使在那些支持它们的体系结构上,编译器仍可能假设所有取消引用的地址在生成代码时都正确对齐,尤其是在优化下。 如果您曾经使用 UBSan 运行您的代码,它也会报错。

第二个原因是严格的别名。 C 语言规定所有 memory 必须通过指向字符类型( charsigned charunsigned char )的指针或指向 object 存储在 memory 中的类型的指针访问; 这确保可以假定指向不同类型的指针不会别名(指向同一内存)。 实际上, uint8_t通常是unsigned char的别名,它是字符类型,例外地允许别名任何类型; 到目前为止,这使得严格的别名问题主要是理论上的。 然而,也没有理由冒这个风险,因为避免它是如此容易和便宜。

你需要投射你的指针,像这样:

int64_t sender_time_us = *(int64_t*)(rx_buffer + 1);

实际上,您只会获得一个字节的数据。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM