我正在研究一种以大端格式将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;
}
这适用于将此功能编译为:
的clangbswapq %rdi
movq %rdi, (%rsi)
retq
但GCC fails to detect the byte swap。我尝试了几种不同的方法,但它们只会让事情变得更糟。我知道GCC可以使用按位和,移位和按位来检测字节交换,但是为什么写字节时它不起作用?
修改:我找到了相应的GCC bug。
答案 0 :(得分: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));
}
-O3
encode_bigend_u64(unsigned long, void*):
bswapq %rdi
movq %rdi, (%rsi)
retq
-O3 -march=native
encode_bigend_u64(unsigned long, void*):
movbeq %rdi, (%rsi)
retq
-O3
encode_bigend_u64(unsigned long, void*):
bswap %rdi
movq %rdi, (%rsi)
ret
-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中的答案来检测系统的字节顺序并添加if
。 union和指针转换版本都有效,并且由gcc
和clang
优化,导致完全相同的程序集(没有分支)。 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 Vol.2A):
MOVBE-交换字节后移动数据
对从第二个复制的数据执行字节交换操作 操作数(源操作数)并将结果存储在第一个操作数中 (目的地操作数)。 [...]
MOVBE指令用于交换读取的字节 从内存或写入内存;从而提供支持 将little-endian值转换为big-endian格式,反之亦然。
答案 1 :(得分:5)
本答案中的所有函数都使用Godbolt Compiler Explorer
上的asm输出 GNU C has a 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
即使在-O1
来自这些函数,您也可以安全地获得良好的代码,并且当movbe
设置为支持该insn的CPU时,它们会使用-march
。
如果你的目标是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),以实现主机到BE和主机到LE功能。当__builtin_bswap
或x86 asm不可用时bswap used by glibc使用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 $8, %rcx
orq %rax, %rcx
... completely naive implementation
htonll
bswap
编译为两个32位union { uint64_t a; uint8_t b[8]; }
和shift /或。这种情况很糟糕,但对于gcc或clang都不是很糟糕。
我对OP代码的bswap
版本没有任何好运。 clang仍然将其编译为64位*
,但我认为使用gcc编译更糟糕的代码。 (参见godbolt链接)。
答案 2 :(得分:2)
我喜欢彼得的解决方案,但是你可以在Haswell上使用其他东西。 Haswell有movbe
指令,那里有3个uops(不比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
的对齐对我来说并不清楚。
通常,预处理器宏会保护某个功能。我希望__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
...