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

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

 #include  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; } 

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

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

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

编辑:我发现了相应的GCC错误 。

这似乎可以解决问题:

 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 with -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处理器)


如果您想要一个适用于大端架构的function,您可以使用此处的答案来检测系统的字节顺序并添加if 。 union和指针转换版本都工作,并且由gccclang优化,导致完全相同的程序集 (没有分支)。 关于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)); } 

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

MOVBE-交换字节后移动数据

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

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

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


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

glibc提供htobe64htole64以及与BE和LE函数类似的主机交换与否,具体取决于机器的字节顺序。 请参阅的文档。 该手册页说他们在版本2.9(2008-11发布)中被添加到glibc中。

 #define _BSD_SOURCE /* See feature_test_macros(7) */ #include  #include  // 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 

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


如果你的目标是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。 预处理器宏可用于选择是否交换( 如glibc ),以实现主机到BE和主机到LEfunction。 当__builtin_bswap或x86 asm不可用时,glibc使用的bswap使用 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 

来自这个答案的htonll编译为两个32位bswap s和shift /或。 这种糟糕,但无论是gcc还是clang都不是很糟糕。


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

我喜欢彼得的解决方案,但这里还有你可以在Haswell上使用的其他东西。 Haswell有movbe指令,那里有3个bswap r64 (没比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修改寄存器中的值并让编译器加载/存储它。


我将函数更改为返回value因为vdest对齐对我来说并不清楚。

通常,预处理器宏会保护一个function。 我希望__MOVBE__用于movbefunction标志,但它不存在( 这台机器有这个function ):

 $ 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 ...