¿Cómo hacer que GCC genere instrucciones bswap para la tienda big endian sin componentes integrados?
x86 compiler-optimization (3)
Estoy trabajando en una función que almacena un valor de 64 bits en la memoria en formato big endian. Tenía la esperanza de poder escribir código C99 portátil que funcione tanto en plataformas endian pequeñas como grandes y hacer que los compiladores x86 modernos generen una instrucción bswap
automáticamente sin ningún componente o intrínseco . Así que empecé con la siguiente función:
#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;
}
Esto funciona bien para el clang que compila esta función para:
bswapq %rdi
movq %rdi, (%rsi)
retq
Pero GCC no detecta el intercambio de bytes . Probé un par de enfoques diferentes pero solo empeoraron las cosas. Sé que GCC puede detectar intercambios de bytes usando bitwise-and, shift y bitwise-o, pero ¿por qué no funciona cuando se escriben bytes?
Edición: encontré el correspondiente error GCC .
Esto parece hacer el truco:
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));
}
clang con -O3
encode_bigend_u64(unsigned long, void*):
bswapq %rdi
movq %rdi, (%rsi)
retq
clang con -O3 -march=native
encode_bigend_u64(unsigned long, void*):
movbeq %rdi, (%rsi)
retq
gcc con -O3
encode_bigend_u64(unsigned long, void*):
bswap %rdi
movq %rdi, (%rsi)
ret
gcc con -O3 -march=native
encode_bigend_u64(unsigned long, void*):
movbe %rdi, (%rsi)
ret
Probado con clang 3.8.0 y gcc 5.3.0 en http://gcc.godbolt.org/ (así que no sé exactamente qué procesador está debajo (para -march=native
) pero sospecho que hay un procesador x86_64 reciente )
Si desea una función que también funcione para las arquitecturas de big endian, puede usar las respuestas desde here para detectar el endianness del sistema y agregar un if
. Tanto la versión de la unión como la del puntero funcionan y están optimizadas por gcc
y clang
dando como resultado el mismo ensamblaje (sin ramas). Código completo en 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));
}
Referencia del conjunto de instrucciones de las arquitecturas Intel® 64 e IA-32 (3-542 Vol. 2A):
MOVBE: mover datos después de intercambiar bytes
Realiza una operación de intercambio de bytes en los datos copiados del segundo operando (operando de origen) y almacena el resultado en el primer operando (operando de destino). [...]
La instrucción MOVBE se proporciona para intercambiar los bytes en una lectura de la memoria o en una escritura en la memoria; proporcionando así soporte para convertir valores de little-endian al formato big-endian y viceversa.
Me gusta la solución de Peter, pero aquí hay algo más que puedes usar en Haswell. Haswell tiene la instrucción movbe
, que es 3 uops allí (no más barato que bswap r64
+ una carga o tienda normal), pero es más rápido en 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;
}
Úsalo con algo como uint64_t tmp = load_bigend_u64(array[i]);
Podría revertir esto para hacer una función store_bigend
, o usar bswap
para modificar un valor en un registro y dejar que el compilador lo cargue / almacene.
vdest
la función para devolver el value
porque la alineación de vdest
no estaba clara para mí.
Por lo general, una característica está protegida por una macro preprocesadora. Espero que se __MOVBE__
para el movbe
función movbe
, pero no está presente ( esta máquina tiene la función ):
$ 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
...
Todas las funciones en esta respuesta con salida de asm en el Explorador del compilador de Godbolt
GNU C tiene un uint64_t __builtin_bswap64 (uint64_t x)
, desde GNU C 4.3. Aparentemente, esta es la forma más confiable de hacer que gcc / clang genere un código que no apesta para esto .
glibc proporciona htobe64
, htole64
y host similar a / desde las funciones BE y LE que intercambian o no, dependiendo de la endianidad de la máquina. Consulte los documentos para <endian.h>
. La página del manual dice que se agregaron a glibc en la versión 2.9 (lanzada 2008-11).
#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
Con seguridad obtiene un buen código incluso en -O1
de esas funciones , y usan movbe
cuando -march
se configura en una CPU que soporta esa información.
Si está apuntando a GNU C, pero no a glibc, puede tomar prestada la definición de glibc (sin embargo, recuerde que es un código LGPL):
#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__
Si realmente necesita un respaldo que pueda compilarse bien en compiladores que no sean compatibles con las versiones C de GNU, el código de la respuesta de @bolov podría usarse para implementar un intercambio de recursos que se compile adecuadamente. Las macros del preprocesador podrían utilizarse para elegir si intercambiar o no ( como lo hace glibc ), para implementar las funciones de host a BE y de host a LE. El bswap utilizado por glibc cuando __builtin_bswap
o x86 asm no está disponible usa el lenguaje de enmascarar y cambiar que bolov encontró que era bueno. gcc lo reconoce mejor que simplemente cambiando.
El código de esta publicación del blog Endian-agnostic coding se compila en bswap con gcc, pero no con clang . IDK si hay algo que ambos reconocedores de patrones reconocerán.
// 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
El htonll
de esta respuesta se compila en dos bswap
32 bswap
combinados con shift / o. Este tipo de chupa, pero no es terrible con gcc o clang.
No tuve suerte con un union { uint64_t a; uint8_t b[8]; }
union { uint64_t a; uint8_t b[8]; }
union { uint64_t a; uint8_t b[8]; }
versión del código del OP. clang todavía lo compila en un bswap
64 bswap
, pero creo que compila a un código aún peor con gcc. (Ver el enlace de Godbolt).