【问题标题】:Preferred idiom for endianess-agnostic reads字节顺序无关读取的首选习语
【发布时间】:2014-10-02 21:14:35
【问题描述】:

在 Plan 9 源代码中,我经常找到这样的代码,用于从具有明确定义的字节顺序的缓冲区中读取序列化数据:

#include <stdint.h>

uint32_t le32read(uint8_t buf[static 4]) {
    return (buf[0] | buf[1] << 8 | buf[2] << 16 | buf[3] << 24);
}

我希望 gcc 和 clang 都能将这段代码编译成像 amd64 上的这个程序集一样简单的东西:

    .global le32read
    .type le32read,@function
le32read:
    mov (%rdi),%eax
    ret
    .size le32read,.-le32read

但与我的预期相反,gcc 和 clang 都没有认识到这种模式,而是产生了具有多个班次的复杂程序集。

对于这种操作,是否有一个习惯用法既可以移植到所有 C99 实现,又可以跨实现生成良好(即像上面介绍的那样)代码?

【问题讨论】:

  • 您没有指定您尝试编译的优化级别。你试过 -O2 吗?
  • @JonathonReinhart 我使用 -O3 编译,如果出现 clang,我还检查了显示初始化未完成的 llvm 中间代码。
  • 我假设您的意思是uint8_t buf[4](没有static,这没有任何意义)。作为参数,数组被解释为指针,所以它就是uint8_t *buf。所以编译器必须通过指针访问内存,而不是通过变量。当涉及到访问内存时,当涉及到编译器可以做出的假设时,事情就变得更加棘手了,因为可以通过不同的方式(指针算法,编译器的外部方式)访问内存。即使这种特殊情况看起来微不足道,但作为一般情况,这是不可能的。
  • @bolov uint8_t buf[static 4] 是一种新语法,它断言编译器访问buf 的最多四个元素是明确定义的。我专门讨论了 x86 上的程序集是什么样子的,因为 x86 允许各种未对齐的内存访问,这使得编译器优化变得更加容易——在大多数情况下,编译器不必对内存对齐做出任何假设。我看不出这种优化将如何成为未定义的行为(在 amd64 上)。
  • @bolov 我上面链接的代码使用 gcc 编译为 20 条指令。二十条指令以明确定义的字节顺序从内存中读取单个整数,为此有专门的指令(即bswap),因此可以在一两条指令中完成。大多数时候,我正在操作的数据已经在 L3 缓存中,因为它是在几个周期前从其他地方(即网络、文件系统、解压缩器)获取的。即使内存主导了这些操作的运行时,它们也经常出现,足以要求编译器正确优化它们。

标签: c optimization endianness idioms


【解决方案1】:

您可以像this answer 那样确定字节顺序。然后使用O32_HOST_ORDER 宏来决定是直接将字节数组转换为uint32_t 还是使用您的位移表达式。

#include <stdint.h>

uint32_t le32read(uint8_t buf[static 4]) {
    if (O32_HOST_ORDER == O32_LITTLE_ENDIAN) {
        return *(uint32_t *)&buf[0];
    }
    return (buf[0] | buf[1] << 8 | buf[2] << 16 | buf[3] << 24);
}

【讨论】:

  • 这是不可移植的,因为我不能保证 buf 的对齐等于特定平台对 uint32_t 所需的对齐。某些平台(如 MIPS 和较旧的 ARM)不允许您进行未对齐的内存访问。因此,编译器的工作就是像我想要执行的那样优化读取——只有编译器才能做到这一点,因为只有它知道平台对齐限制。
  • “不可移植”是一个非常宽容的词。这实际上是未定义的行为,因为它违反了严格的别名规则。
  • 是的,很遗憾,如果您正在编写任何重要的代码,您需要指定 -fno-strict-aliasing
  • @tmyklebu 你可以在没有-fno-strict-aliasing 的情况下做所有这些事情。对于不懂编程的人来说,该标志是一种作弊。
  • @FUZxxl:耸耸肩。我不知道如何在没有-fno-strict-aliasing 或导致不必要的复制或单独的read() 对标头和内容。
【解决方案2】:

如果你想保证原生平台订单和定义订单(例如网络订单)之间的转换,你可以让系统库来工作,只需使用&lt;netinet/in.h&gt;的功能:hton、htons、htonl还有ntoh,ntohs,nthol。

但我必须承认包含文件不能保证:在 Windows 下我认为是winsock.h

【讨论】:

  • 这些函数的问题在于它们所做的事情没有明确说明,因为它们使用特定于平台的类型,例如shortint。我什至不确定它们是否对我的目的有用。
  • 在我发现的所有实现中,htonsntohsint16_t(或 uint16_t)和 htonlntohl 上运行 int32_t(或 @ 987654332@)。毕竟它们用于那些知道何时需要 16 位或 32 位值的协议......
  • 啊,我的知识在那里已经过时了。尽管如此,该标准并没有规定uint32_t 在内存中的布局方式。将结构覆盖在从某个地方获得的缓冲区上是一项值得商榷的尝试。
  • @FUZxxl:定义一组标准化的可移植宏不会有语义上的困难,这些宏会将多个类型存储到较小类型的数组中,具有字节序和位数每个目标元素由宏而不是机器的变量类型定义[因此unsigned short dest[2]; __pack_le32us(0x12345678, dest); 将设置dest[0] 为0x5678 和dest[1] 为0x1234,即使short 是24 位类型]。如果有一个标准,使宏识别编译器内在函数的编译器可以轻松优化它们。
【解决方案3】:

经过一些研究,我发现(在 Freenode 上##c 中的好人的帮助下),gcc 5.0 将对上述类型的模式进行优化。事实上,它将我的问题中列出的 C 源代码编译为我在下面列出的确切程序集。

我没有找到关于clang的类似信息,所以我提交了bug report截至Clang 9.0,clang 识别读取和写入习语并将其转换为快速代码。

【讨论】:

    猜你喜欢
    • 2013-08-23
    • 1970-01-01
    • 1970-01-01
    • 2021-09-07
    • 2021-07-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-10-19
    相关资源
    最近更新 更多