如何让 GCC 在没有内置函数的情况下为大端存储生成 bswap 指令?

How to make GCC generate bswap instruction for big endian store without builtins?

更新:这已在 GCC 8.1 中修复。

我正在研究一个以大端格式将 64 位值存储到内存中的函数。我希望我可以编写 可以在小端和大端平台上运行的可移植 C99 代码 并让现代 x86 编译器自动生成 bswap 指令 而无需任何内置函数或内在函数。所以我开始使用以下功能:

#include <stdint.h>

void
encode_bigend_u64(uint64_t value, void *vdest) {
    uint8_t *bytes = (uint8_t *)vdest;
    bytes[0] = value >> 56;
    bytes[1] = value >> 48;
    bytes[2] = value >> 40;
    bytes[3] = value >> 32;
    bytes[4] = value >> 24;
    bytes[5] = value >> 16;
    bytes[6] = value >> 8;
    bytes[7] = value;
}

这适用于将此函数编译为的 clang:

bswapq  %rdi
movq    %rdi, (%rsi)
retq

但是 GCC fails to detect the byte swap。我尝试了几种不同的方法,但它们只会让事情变得更糟。我知道 GCC 可以使用按位与、移位和按位或来检测字节交换,但为什么它在写入字节时不起作用?

编辑:找到对应的GCC bug.

这似乎可以解决问题:

void encode_bigend_u64(uint64_t value, void* dest)
{
  value =
      ((value & 0xFF00000000000000u) >> 56u) |
      ((value & 0x00FF000000000000u) >> 40u) |
      ((value & 0x0000FF0000000000u) >> 24u) |
      ((value & 0x000000FF00000000u) >>  8u) |
      ((value & 0x00000000FF000000u) <<  8u) |      
      ((value & 0x0000000000FF0000u) << 24u) |
      ((value & 0x000000000000FF00u) << 40u) |
      ((value & 0x00000000000000FFu) << 56u);
  memcpy(dest, &value, sizeof(uint64_t));
}

叮当声 -O3

encode_bigend_u64(unsigned long, void*):
        bswapq  %rdi
        movq    %rdi, (%rsi)
        retq

clang 与 -O3 -march=native

encode_bigend_u64(unsigned long, void*):
        movbeq  %rdi, (%rsi)
        retq

gcc 与 -O3

encode_bigend_u64(unsigned long, void*):
        bswap   %rdi
        movq    %rdi, (%rsi)
        ret

gcc 与 -O3 -march=native

encode_bigend_u64(unsigned long, void*):
        movbe   %rdi, (%rsi)
        ret

http://gcc.godbolt.org/ 上使用 clang 3.8.0 和 gcc 5.3.0 进行了测试(所以我不知道到底是什么处理器(对于 -march=native),但我强烈怀疑最近 x86_64处理器)


如果您想要一个也适用于大端架构的函数,您可以使用 here to detect the endianness of the system and add an if. Both the union and the pointer casts versions work and are optimized by both gcc and clang resulting in the exact same assembly (no branches). Full code on godebolt:

中的答案
int is_big_endian(void)
{
    union {
        uint32_t i;
        char c[4];
    } bint = {0x01020304};

    return bint.c[0] == 1;
}

void encode_bigend_u64_union(uint64_t value, void* dest)
{
  if (!is_big_endian())
    //...
  memcpy(dest, &value, sizeof(uint64_t));
}

Intel® 64 and IA-32 Architectures Instruction Set Reference(3-542 第 2A 卷):

MOVBE—Move Data After Swapping Bytes

Performs a byte swap operation on the data copied from the second operand (source operand) and store the result in the first operand (destination operand). [...]

The MOVBE instruction is provided for swapping the bytes on a read from memory or on a write to memory; thus providing support for converting little-endian values to big-endian format and vice versa.

此答案中的所有函数在 Godbolt Compiler Explorer

上具有 asm 输出

GNU C has a uint64_t __builtin_bswap64 (uint64_t x),从 GNU C 4.3 开始。 这显然是让 gcc / clang 生成的代码最可靠的方法

glibc 提供 htobe64htole64 和类似的主机 to/from BE 和 LE 函数,根据机器的字节顺序交换或不交换。请参阅 <endian.h> 的文档。手册页说它们是在 2.9 版(2008-11 发布)中添加到 glibc 中的。

#define _BSD_SOURCE             /* See feature_test_macros(7) */

#include <stdint.h>

#include <endian.h>
// ideal code with clang from 3.0 onwards, probably earlier
// ideal code with gcc from 4.4.7 onwards, probably earlier
uint64_t load_be64_endian_h(const uint64_t *be_src) { return be64toh(*be_src); }
    movq    (%rdi), %rax
    bswap   %rax

void store_be64_endian_h(uint64_t *be_dst, uint64_t data) { *be_dst = htobe64(data); }
    bswap   %rsi
    movq    %rsi, (%rdi)

// check that the compiler understands the data movement and optimizes away a double-conversion (which inline-asm `bswap` wouldn't)
// it does optimize away with gcc 4.9.3 and later, but not with gcc 4.9.0 (2x bswap)
// optimizes away with clang 3.7.0 and later, but not clang 3.6 or earlier (2x bswap)
uint64_t double_convert(uint64_t data) {
  uint64_t tmp;
  store_be64_endian_h(&tmp, data);
  return load_be64_endian_h(&tmp);
}
    movq    %rdi, %rax

即使在 -O1 时,您也可以从这些函数 安全地获得好的代码,并且当 -march 设置为 [时,它们使用 movbe =64=] 支持那个insn.


如果您的目标是 GNU C,而不是 glibc,您可以借用 glibc 的定义(不过请记住它是 LGPLed 代码):

#ifdef __GNUC__
# if __GNUC_PREREQ (4, 3)

static __inline unsigned int
__bswap_32 (unsigned int __bsx) { return __builtin_bswap32 (__bsx);  }

# elif __GNUC__ >= 2
    // ... some fallback stuff you only need if you're using an ancient gcc version, using inline asm for non-compile-time-constant args
# endif  // gcc version
#endif // __GNUC__

如果你真的需要一个可能在不支持 GNU C 内置的编译器上编译良好的回退,@bolov 的答案中的代码可以用来实现一个编译良好的 bswap。预处理器宏可用于选择是否交换(like glibc does), to implement host-to-BE and host-to-LE functions. The bswap used by glibc__builtin_bswap 或 x86 asm 不可用时使用 bolov 发现的掩码和移位习惯用法很好。gcc 识别它比仅仅移动更好。


来自 this Endian-agnostic coding blog post 的代码使用 gcc 编译为 bswap,但使用 clang 编译为 。 IDK 如果他们的模式识别器都可以识别任何东西。

// Note that this is a load, not a store like the code in the question.
uint64_t be64_to_host(unsigned char* data) {
    return
      ((uint64_t)data[7]<<0)  | ((uint64_t)data[6]<<8 ) |
      ((uint64_t)data[5]<<16) | ((uint64_t)data[4]<<24) |
      ((uint64_t)data[3]<<32) | ((uint64_t)data[2]<<40) |
      ((uint64_t)data[1]<<48) | ((uint64_t)data[0]<<56);
}

    ## gcc 5.3 -O3 -march=haswell
    movbe   (%rdi), %rax
    ret

    ## clang 3.8 -O3 -march=haswell
    movzbl  7(%rdi), %eax
    movzbl  6(%rdi), %ecx
    shlq    , %rcx
    orq     %rax, %rcx
    ... completely naive implementation

this answer 中的 htonll 编译为两个 32 位 bswap 与 shift/or 组合。这种很糟糕,但对于 gcc 或 clang 来说并不可怕。


我对 OP 代码的 union { uint64_t a; uint8_t b[8]; } 版本没有任何好感。 clang 仍然将其编译为 64 位 bswap,但我认为使用 gcc 编译为更糟糕的代码。 (见神箭link)。

我喜欢 Peter 的解决方案,但这里还有一些您可以在 Haswell 上使用的方法。 Haswell 有 movbe 指令,那里是 3 微指令(不比 bswap r64 + 正常加载或存储便宜),但在 Atom / Silvermont 上更快(https://agner.org/optimize/):

// AT&T syntax, compile without -masm=intel
inline
uint64_t load_bigend_u64(uint64_t value)
{
    __asm__ ("movbe %[src], %[dst]"   // x86-64 only
             :  [dst] "=r" (value)
             :  [src] "m" (value)
            );
    return value;
}

将它与类似 uint64_t tmp = load_bigend_u64(array[i]);

的东西一起使用

您可以反转它以创建一个 store_bigend 函数,或者使用 bswap 修改寄存器中的值并让编译器 load/store 它。


我将函数更改为 return value 因为 vdest 的对齐方式对我来说不是很清楚。

通常一个特征由预处理器宏保护。我希望 __MOVBE__ 用于 movbe 功能标志,但它不存在 (this machine has the feature):

$ gcc -march=native -dM -E - < /dev/null | sort
...
#define __LWP__ 1
#define __LZCNT__ 1
#define __MMX__ 1
#define __MWAITX__ 1
#define __NO_INLINE__ 1
#define __ORDER_BIG_ENDIAN__ 4321
...