Veja também uma versão anterior desta resposta em outra pergunta rotativa com mais alguns detalhes sobre o que o asm gcc / clang produz para x86.
A maneira mais amigável do compilador de expressar uma rotação em C e C ++ que evita qualquer comportamento indefinido parece ser a implementação de John Regehr . Eu o adaptei para girar pela largura do tipo (usando tipos de largura fixa 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 qualquer tipo de inteiro sem sinal, não apenas uint32_t
, para que você possa fazer versões para outros tamanhos.
Veja também uma versão do modelo C ++ 11 com muitas verificações de segurança (incluindo a de static_assert
que a largura do tipo é uma potência de 2) , o que não é o caso em alguns DSPs de 24 bits ou mainframes de 36 bits, por exemplo.
Eu recomendo usar apenas o modelo como back-end para wrappers com nomes que incluem a largura de rotação explicitamente. As regras de promoção de inteiros significam que rotl_template(u16 & 0x11UL, 7)
faria uma rotação de 32 ou 64 bits, não 16 (dependendo da largura de unsigned long
). Even uint16_t & uint16_t
é promovido signed int
pelas regras de promoção de inteiros do C ++, exceto em plataformas onde int
não é mais largo que uint16_t
.
No x86 , esta versão se alinha a um únicorol r32, cl
(ou rol r32, imm8
) com compiladores que o agrupam, porque o compilador sabe que as instruções de rotação e deslocamento do x86 mascaram a contagem de deslocamento da mesma forma que o código-fonte C faz.
Suporte de compilador para este idioma que evita UB em x86, para uint32_t x
e unsigned int n
para mudanças de contagem de variável:
- clang: reconhecido por rotação de contagem variável desde clang3.5, vários turnos + ou insns antes disso.
- gcc: reconhecido por rotação de contagem variável desde gcc4.9 , múltiplos turnos + ou insns antes disso. O gcc5 e posteriores otimizam o branch e a máscara na versão wikipedia também, usando apenas uma instrução
ror
ou rol
para contagens de variáveis.
- icc: compatível com rotações de contagem variável desde ICC13 ou anterior . A contagem constante gira o uso
shld edi,edi,7
que é mais lento e leva mais bytes do que rol edi,7
em alguns processadores (especialmente AMD, mas também alguns Intel), quando o BMI2 não está disponível para rorx eax,edi,25
salvar um MOV.
- MSVC: x86-64 CL19: reconhecido apenas para rotações de contagem constante. (O idioma da Wikipédia é reconhecido, mas o branch e o AND não são otimizados). Use o
_rotl
/ _rotr
intrinsics do <intrin.h>
x86 (incluindo x86-64).
gcc para ARM utiliza um and r1, r1, #31
para rodar variável de contagem, mas ainda faz a rotação real com uma única instrução : ror r0, r0, r1
. Portanto, o gcc não percebe que as contagens de rotação são inerentemente modulares. Como os documentos do ARM dizem, "ROR com comprimento de deslocamento n
, mais de 32 é o mesmo que ROR com comprimento de deslocamento n-32
" . Acho que o gcc fica confuso aqui porque os deslocamentos para a esquerda / direita no ARM saturam a contagem, portanto, um deslocamento de 32 ou mais limpará o registro. (Ao contrário do x86, em que as mudanças mascaram a contagem da mesma forma que as rotações). Ele provavelmente decide que precisa de uma instrução AND antes de reconhecer o idioma de rotação, por causa de como as mudanças não circulares funcionam naquele destino.
Os compiladores x86 atuais ainda usam uma instrução extra para mascarar uma contagem de variável para rotações de 8 e 16 bits, provavelmente pela mesma razão que eles não evitam o AND no ARM. Esta é uma otimização perdida, porque o desempenho não depende da contagem de rotação em qualquer CPU x86-64. (O mascaramento de contagens foi introduzido no 286 por motivos de desempenho, porque ele lida com os turnos de forma iterativa, não com latência constante como as CPUs modernas.)
BTW, prefira girar para a direita para rotações de contagem variável, para evitar que o compilador 32-n
implemente uma rotação para a esquerda em arquiteturas como ARM e MIPS que fornecem apenas uma rotação para a direita. (Isso otimiza com contagens de constantes de tempo de compilação.)
Curiosidade: ARM realmente não tem mudança dedicado / instruções de rotação, é apenas MOV com a fonte operando a atravessar o cano-shifter em modo ROR : mov r0, r0, ror r1
. Portanto, um rotate pode dobrar em um operando de fonte de registro para uma instrução EOR ou algo assim.
Certifique-se de usar tipos não assinados para n
e o valor de retorno, ou então não será uma rotação . (gcc para x86 alvos faz deslocamentos aritméticos para a direita, deslocando as cópias do bit de sinal em vez de zeros, levando a um problema quando você OR
os dois valores deslocados juntos. Deslocamentos para a direita de inteiros com sinal negativo é um comportamento definido pela implementação em C.)
Além disso, certifique-se de que a contagem de deslocamento seja um tipo sem sinal , porque (-n)&31
um tipo com sinal pode ser o complemento ou sinal / magnitude de um, e não o mesmo que o 2 ^ n modular que você obtém com o complemento sem sinal ou de dois. (Veja os comentários na postagem do blog de Regehr). unsigned int
funciona bem em todos os compiladores que eu examinei, para cada largura de x
. Alguns outros tipos realmente derrotam o reconhecimento de idioma para alguns compiladores, portanto, não use apenas o mesmo tipo que x
.
Alguns compiladores fornecem intrínsecos para rotações , o que é muito melhor do que inline-asm se a versão portátil não gerar um bom código no compilador que você está almejando. Não há intrínsecos de plataforma cruzada para nenhum compilador que eu conheça. Estas são algumas das opções do x86:
- Documentos da Intel que
<immintrin.h>
fornecem _rotl
e _rotl64
intrínsecos , e o mesmo para o turno certo. MSVC requer <intrin.h>
, enquanto gcc requer <x86intrin.h>
. An #ifdef
cuida do gcc vs. icc, mas o clang não parece fornecê-los em lugar nenhum, exceto no modo de compatibilidade MSVC com-fms-extensions -fms-compatibility -fms-compatibility-version=17.00
. E o conjunto que emite para eles é uma merda (máscara extra e um CMOV).
- MSVC:
_rotr8
e_rotr16
.
- gcc e icc (não clang):
<x86intrin.h>
também fornece __rolb
/ __rorb
para rotação de 8 bits para a esquerda / direita, __rolw
/ __rorw
(16 bits), __rold
/ __rord
(32 bits), __rolq
/ __rorq
(64 bits, definido apenas para destinos de 64 bits). Para rotações estreitas, a implementação usa __builtin_ia32_rolhi
ou ...qi
, mas as rotações de 32 e 64 bits são definidas usando shift / ou (sem proteção contra UB, porque o código ia32intrin.h
só precisa funcionar no gcc para x86). GNU C parece não ter nenhuma __builtin_rotate
função de plataforma cruzada da maneira que tem __builtin_popcount
(o que se expande para o que for ideal na plataforma de destino, mesmo se não for uma única instrução). Na maioria das vezes, você obtém um bom código de reconhecimento de idioma.
// 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
Presumivelmente, alguns compiladores não x86 também possuem intrínsecos, mas não vamos expandir esta resposta wiki da comunidade para incluir todos eles. (Talvez faça isso na resposta existente sobre intrínsecos ).
(A versão antiga desta resposta sugeria asm embutidas específicas do MSVC (que funciona apenas para código x86 de 32 bits) ou http://www.devx.com/tips/Tip/14043 para uma versão C. Os comentários estão respondendo a isso .)
O conjunto embutido derrota muitas otimizações , especialmente no estilo MSVC, porque força as entradas a serem armazenadas / recarregadas . Uma rotação embutida asm do GNU C cuidadosamente escrita permitiria que a contagem fosse um operando imediato para contagens de deslocamento da constante de tempo de compilação, mas ainda não poderia otimizar totalmente se o valor a ser deslocado também fosse uma constante de tempo de compilação após inlining. https://gcc.gnu.org/wiki/DontUseInlineAsm .