Consulte también una versión anterior de esta respuesta en otra pregunta rotativa con algunos detalles más sobre lo que produce asm gcc / clang para x86.
La forma más fácil de compilar de expresar una rotación en C y C ++ que evita cualquier comportamiento indefinido parece ser la implementación de John Regehr . Lo adapté para rotar por el ancho del tipo (usando tipos de ancho fijo como uint32_t
).
#include <stdint.h> // for uint32_t
#include <limits.h> // for CHAR_BIT
// #define NDEBUG
#include <assert.h>
static inline uint32_t rotl32 (uint32_t n, unsigned int c)
{
const unsigned int mask = (CHAR_BIT*sizeof(n) - 1); // assumes width is a power of 2.
// assert ( (c<=mask) &&"rotate by type width or more");
c &= mask;
return (n<<c) | (n>>( (-c)&mask ));
}
static inline uint32_t rotr32 (uint32_t n, unsigned int c)
{
const unsigned int mask = (CHAR_BIT*sizeof(n) - 1);
// assert ( (c<=mask) &&"rotate by type width or more");
c &= mask;
return (n>>c) | (n<<( (-c)&mask ));
}
Funciona para cualquier tipo de entero sin signo, no solo uint32_t
, por lo que podría hacer versiones para otros tamaños.
Consulte también una versión de plantilla de C ++ 11 con muchas comprobaciones de seguridad (incluida la de static_assert
que el ancho del tipo es una potencia de 2) , que no es el caso en algunos DSP de 24 bits o mainframes de 36 bits, por ejemplo.
Recomendaría usar solo la plantilla como back-end para contenedores con nombres que incluyan explícitamente el ancho de rotación. Las reglas de promoción de enteros significan que rotl_template(u16 & 0x11UL, 7)
haría una rotación de 32 o 64 bits, no 16 (dependiendo del ancho de unsigned long
). Even uint16_t & uint16_t
es promovido signed int
por las reglas de promoción de enteros de C ++, excepto en plataformas donde int
no es más ancho que uint16_t
.
En x86 , esta versión se integra en un solorol r32, cl
(o rol r32, imm8
) con compiladores que lo asimilan, porque el compilador sabe que las instrucciones de rotación y desplazamiento x86 enmascaran el recuento de turnos de la misma manera que lo hace la fuente C.
Soporte del compilador para este modismo que evita UB en x86, para uint32_t x
y unsigned int n
para cambios de conteo variable:
- clang: reconocido por rotaciones de conteo variable desde clang3.5, múltiples turnos + o insns antes de eso.
- gcc: reconocido para rotaciones de conteo variable desde gcc4.9 , múltiples turnos + o insns antes de eso. gcc5 y posteriores también optimizan la rama y la máscara en la versión de wikipedia, usando solo una instrucción
ror
o rol
para recuentos de variables.
- icc: compatible con rotaciones de conteo variable desde ICC13 o antes . El conteo constante rota el uso,
shld edi,edi,7
que es más lento y ocupa más bytes que rol edi,7
en algunas CPU (especialmente AMD, pero también algunas Intel), cuando BMI2 no está disponible para rorx eax,edi,25
guardar un MOV.
- MSVC: x86-64 CL19: solo se reconoce para rotaciones de conteo constante. (Se reconoce el idioma de wikipedia, pero la rama y Y no están optimizados). Utilice
_rotl
/ _rotr
intrinsics desde <intrin.h>
x86 (incluido x86-64).
GCC para ARM utiliza una and r1, r1, #31
para gira variable de conteo, pero todavía lo hace la rotación real con una sola instrucción : ror r0, r0, r1
. Entonces, gcc no se da cuenta de que los conteos rotativos son inherentemente modulares. Como dicen los documentos de ARM, "ROR con longitud de turno n
, más de 32 es lo mismo que ROR con longitud de turno n-32
" . Creo que gcc se confunde aquí porque los cambios de izquierda a derecha en ARM saturan el recuento, por lo que un cambio de 32 o más borrará el registro. (A diferencia de x86, donde los cambios enmascaran el recuento de la misma manera que los giros). Probablemente decida que necesita una instrucción AND antes de reconocer el idioma de rotación, debido a cómo funcionan los turnos no circulares en ese objetivo.
Los compiladores x86 actuales todavía usan una instrucción adicional para enmascarar un recuento de variables para rotaciones de 8 y 16 bits, probablemente por la misma razón por la que no evitan el AND en ARM. Esta es una optimización perdida, porque el rendimiento no depende del recuento de rotaciones en ninguna CPU x86-64. (El enmascaramiento de recuentos se introdujo con 286 por razones de rendimiento porque manejaba los cambios de forma iterativa, no con latencia constante como las CPU modernas).
Por cierto, prefiera rotar a la derecha para rotaciones de recuento variable, para evitar que el compilador 32-n
implemente una rotación a la izquierda en arquitecturas como ARM y MIPS que solo proporcionan una rotación a la derecha. (Esto se optimiza con conteos de constantes de tiempo de compilación).
Dato curioso: ARM no tiene realmente turno dedicada / instrucciones de rotación, es sólo MOV con la fuente de operando de pasar por el cañón-palanca de cambios en el modo de ROR : mov r0, r0, ror r1
. Entonces, una rotación puede plegarse en un operando de fuente de registro para una instrucción EOR o algo así.
Asegúrese de usar tipos sin firmar para n
y el valor de retorno, o de lo contrario no será una rotación . (gcc para destinos x86 hace cambios aritméticos a la derecha, cambiando copias del bit de signo en lugar de ceros, lo que genera un problema cuando se OR
cambian los dos valores juntos. Los cambios a la derecha de enteros con signo negativo es un comportamiento definido por la implementación en C.)
Además, asegúrese de que el recuento de turnos sea un tipo sin signo , porque (-n)&31
con un tipo con signo podría ser el complemento de uno o el signo / magnitud, y no el mismo que el 2 ^ n modular que obtiene con el complemento de dos o sin signo. (Ver comentarios en la publicación del blog de Regehr). unsigned int
funciona bien en todos los compiladores que he visto, para cada ancho de x
. Algunos otros tipos en realidad anulan el reconocimiento idiomático de algunos compiladores, así que no use simplemente el mismo tipo que x
.
Algunos compiladores proporcionan elementos intrínsecos para rotaciones , que es mucho mejor que inline-asm si la versión portátil no genera un buen código en el compilador al que se dirige. No hay elementos intrínsecos multiplataforma para ningún compilador que yo conozca. Estas son algunas de las opciones de x86:
- Intel
<immintrin.h>
proporciona_rotl
_rotl64
documentos e intrínsecos , y lo mismo para el turno correcto. MSVC requiere <intrin.h>
, mientras que gcc requiere <x86intrin.h>
. An #ifdef
se encarga de gcc frente a icc, pero clang no parece proporcionarlos en ninguna parte, excepto en el modo de compatibilidad MSVC con-fms-extensions -fms-compatibility -fms-compatibility-version=17.00
. Y el conjunto que emite para ellos apesta (enmascaramiento extra y un CMOV).
- MSVC:
_rotr8
y_rotr16
.
- gcc y icc (no clang):
<x86intrin.h>
también proporciona __rolb
/ __rorb
para rotación izquierda / derecha de 8 bits, __rolw
/ __rorw
(16 bits), __rold
/ __rord
(32 bits), __rolq
/ __rorq
(64 bits, solo definido para destinos de 64 bits). Para rotaciones estrechas, la implementación usa __builtin_ia32_rolhi
o ...qi
, pero las rotaciones de 32 y 64 bits se definen usando shift / o (sin protección contra UB, porque el código ia32intrin.h
solo tiene que funcionar en gcc para x86). GNU C parece no tener ninguna __builtin_rotate
función multiplataforma de la forma en que lo hace __builtin_popcount
(lo que se expande a lo que sea óptimo en la plataforma de destino, incluso si no es una sola instrucción). La mayoría de las veces se obtiene un buen código del reconocimiento de idiomas.
// For real use, probably use a rotate intrinsic for MSVC, or this idiom for other compilers. This pattern of #ifdefs may be helpful
#if defined(__x86_64__) || defined(__i386__)
#ifdef _MSC_VER
#include <intrin.h>
#else
#include <x86intrin.h> // Not just <immintrin.h> for compilers other than icc
#endif
uint32_t rotl32_x86_intrinsic(rotwidth_t x, unsigned n) {
//return __builtin_ia32_rorhi(x, 7); // 16-bit rotate, GNU C
return _rotl(x, n); // gcc, icc, msvc. Intel-defined.
//return __rold(x, n); // gcc, icc.
// can't find anything for clang
}
#endif
Es de suponer que algunos compiladores que no son x86 también tienen elementos intrínsecos, pero no ampliemos esta respuesta de la wiki comunitaria para incluirlos a todos. (Tal vez haga eso en la respuesta existente sobre intrínsecos ).
(La versión anterior de esta respuesta sugería un conjunto en línea específico de MSVC (que solo funciona para código x86 de 32 bits), o http://www.devx.com/tips/Tip/14043 para una versión C. Los comentarios responden a eso .)
Asm en línea derrota muchas optimizaciones , especialmente el estilo MSVC porque obliga a que las entradas se almacenen / recarguen . Una rotación de ensamblaje en línea de GNU C cuidadosamente escrita permitiría que el recuento sea un operando inmediato para los recuentos de cambios constantes en tiempo de compilación, pero aún así no se podría optimizar por completo si el valor que se va a cambiar es también una constante en tiempo de compilación después de la alineación. https://gcc.gnu.org/wiki/DontUseInlineAsm .