如何让 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 提供 htobe64
、htole64
和类似的主机 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
...
更新:这已在 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 提供 htobe64
、htole64
和类似的主机 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
...