如何让GCC为没有内置的大端存储生成bswap指令?

nwe*_*hof 19 c x86 gcc endianness compiler-optimization

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

#include <stdint.h>

void
encode_bigend_u64(uint64_t value, void *vdest) {
    uint64_t bigend;
    uint8_t *bytes = (uint8_t*)&bigend;
    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;
    uint64_t *dest = (uint64_t*)vdest;
    *dest = bigend;
}
Run Code Online (Sandbox Code Playgroud)

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

bswapq  %rdi
movq    %rdi, (%rsi)
retq
Run Code Online (Sandbox Code Playgroud)

但是GCC 无法检测到字节交换.我尝试了几种不同的方法,但它们只会让事情变得更糟.我知道GCC可以使用按位和,移位和按位来检测字节交换,但是为什么写字节时它不起作用?

编辑:我找到了相应的GCC错误.

bol*_*lov 15

这似乎可以解决问题:

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));
}
Run Code Online (Sandbox Code Playgroud)

铿锵有笑 -O3

encode_bigend_u64(unsigned long, void*):
        bswapq  %rdi
        movq    %rdi, (%rsi)
        retq
Run Code Online (Sandbox Code Playgroud)

铿锵有笑 -O3 -march=native

encode_bigend_u64(unsigned long, void*):
        movbeq  %rdi, (%rsi)
        retq
Run Code Online (Sandbox Code Playgroud)

gcc用 -O3

encode_bigend_u64(unsigned long, void*):
        bswap   %rdi
        movq    %rdi, (%rsi)
        ret
Run Code Online (Sandbox Code Playgroud)

gcc用 -O3 -march=native

encode_bigend_u64(unsigned long, void*):
        movbe   %rdi, (%rsi)
        ret
Run Code Online (Sandbox Code Playgroud)

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


如果你想要一个适用于大端架构的功能,你可以使用这里的答案来检测系统的字节序并添加一个if.union和指针转换版本都工作,并由两者优化gcc,clang导致完全相同的程序集(没有分支).关于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));
}
Run Code Online (Sandbox Code Playgroud)

英特尔 ®64 和IA-32架构指令集参考(3-542卷2A):

MOVBE-交换字节后移动数据

对从第二个操作数(源操作数)复制的数据执行字节交换操作,并将结果存储在第一个操作数(目标操作数)中.[...]

MOVBE指令用于交换从存储器读取或写入存储器的字节; 从而为将little-endian值转换为big-endian格式提供支持,反之亦然.


Pet*_*des 5

本回答中的所有函数都使用Godbolt Compiler Explorer上的asm输出


GNU C有一个uint64_t __builtin_bswap64 (uint64_t x),因为GNU C 4.3. 这显然是让gcc/clang生成代码的最可靠方法.

glibc的提供htobe64,htole64以及类似的主机/从该交换与否,取决于机器的字节序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
Run Code Online (Sandbox Code Playgroud)

即使在-O1这些函数中,您也可以安全地获得良好的代码,并且它们movbe-march设置为支持该insn的CPU 时使用.


如果你的目标是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__
Run Code Online (Sandbox Code Playgroud)

如果你真的需要,可能在不支持GNU C编译器内建命令编译以及回退,从@ bolov的答案的代码可以用来实现很好的编译一个BSWAP.预处理器宏可用于选择是否交换(如glibc),以实现主机到BE和主机到LE功能.该由glibc的使用BSWAP__builtin_bswap或x86汇编不可使用该bolov发现好面具和推进成语.gcc认识到它比转移更好.


这个与Endian无关的编码博客文章的代码用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    $8, %rcx
    orq     %rax, %rcx
    ... completely naive implementation
Run Code Online (Sandbox Code Playgroud)

htonll该答案编译两个32位的bswaps的移位/或组合.这种糟糕,但无论是gcc还是clang都不是很糟糕.


union { uint64_t a; uint8_t b[8]; }对OP的代码版本没有任何好运.clang仍将其编译为64位bswap,但我认为用gcc编译甚至更糟糕的代码.(参见godbolt链接).