Eu tenho tentado aprender C no meu tempo livre, e outras linguagens (C #, Java etc.) têm o mesmo conceito (e geralmente os mesmos operadores) ...
O que eu estou querendo saber é, a nível central, o que faz-bit deslocando ( <<
, >>
, >>>
) fazer, quais os problemas que ele pode ajudar a resolver, eo que gotchas espreitam ao redor da curva? Em outras palavras, um guia para iniciantes absolutos para mudar de bits em toda a sua bondade.
operators
bit-manipulation
bit-shift
binary-operators
John Rudy
fonte
fonte
Respostas:
Os operadores de troca de bits fazem exatamente o que o nome indica. Eles trocam bits. Aqui está uma breve introdução (ou não tão breve) aos diferentes operadores de turno.
Os operadores
>>
é o operador aritmético (ou assinado) do deslocamento à direita.>>>
é o operador de mudança à direita lógico (ou não assinado).<<
é o operador de turno esquerdo e atende às necessidades de turnos lógicos e aritméticos.Todos estes operadores pode ser aplicada aos valores inteiros (
int
,long
, possivelmente,short
ebyte
ouchar
). Em alguns idiomas, a aplicação dos operadores shift a qualquer tipo de dados menor queint
redimensiona automaticamente o operando para ser umint
.Observe que
<<<
não é um operador, porque seria redundante.Observe também que C e C ++ não fazem distinção entre os operadores de turno certo . Eles fornecem apenas o
>>
operador, e o comportamento de mudança à direita é a implementação definida para tipos assinados. O restante da resposta usa os operadores C # / Java.(Em todas as implementações C e C ++ convencionais, incluindo GCC e Clang / LLVM, os
>>
tipos assinados são aritméticos. Alguns códigos assumem isso, mas não é algo que o padrão garante. Porém, não é indefinido ; o padrão exige implementações para defini-lo como um No entanto, as mudanças à esquerda de números com sinal negativo não são um comportamento indefinido (estouro de número inteiro assinado). Portanto, a menos que você precise de uma mudança aritmética à direita, geralmente é uma boa ideia fazer a troca de bits com tipos não assinados.)Deslocamento para a esquerda (<<)
Os números inteiros são armazenados, na memória, como uma série de bits. Por exemplo, o número 6 armazenado como um de 32 bits
int
seria:Mudar esse padrão de bit para a posição esquerda (
6 << 1
) resultaria no número 12:Como você pode ver, os dígitos foram deslocados para a esquerda em uma posição e o último dígito à direita é preenchido com um zero. Você também pode observar que o deslocamento para a esquerda é equivalente à multiplicação por potências de 2. Portanto,
6 << 1
é equivalente a6 * 2
e6 << 3
é equivalente a6 * 8
. Um bom compilador de otimização substituirá multiplicações por turnos quando possível.Mudança não circular
Observe que estes não são turnos circulares. Deslocando esse valor para a esquerda em uma posição (
3,758,096,384 << 1
):resulta em 3.221.225.472:
O dígito que é deslocado "no final" é perdido. Não envolve.
Deslocamento lógico para a direita (>>>)
Um deslocamento lógico para a direita é o inverso para o deslocamento para a esquerda. Em vez de mover bits para a esquerda, eles simplesmente se movem para a direita. Por exemplo, mudando o número 12:
à direita em uma posição (
12 >>> 1
) retornará nosso 6 original:Portanto, vemos que mudar para a direita é equivalente à divisão por potências de 2.
Pedaços perdidos se foram
No entanto, uma mudança não pode recuperar bits "perdidos". Por exemplo, se mudarmos esse padrão:
para a esquerda 4 posições (
939,524,102 << 4
), temos 2.147.483.744:e, em seguida, mudando de volta (
(939,524,102 << 4) >>> 4
), obtemos 134.217.734:Não podemos recuperar nosso valor original depois que perdemos os bits.
Desvio aritmético para a direita (>>)
O deslocamento aritmético para a direita é exatamente igual ao deslocamento lógico para a direita, exceto que, em vez de preencher com zero, ele almofada com o bit mais significativo. Isso ocorre porque o bit mais significativo é o bit de sinal ou o bit que distingue números positivos e negativos. Ao preencher com o bit mais significativo, a mudança aritmética para a direita preserva os sinais.
Por exemplo, se interpretarmos esse padrão de bits como um número negativo:
nós temos o número -2.147.483.552. Mudar isso para as 4 posições corretas com o deslocamento aritmético (-2.147.483.552 >> 4) nos daria:
ou o número -134.217.722.
Portanto, vemos que preservamos o sinal de nossos números negativos, usando o deslocamento aritmético para a direita, em vez do deslocamento lógico para a direita. E mais uma vez, vemos que estamos realizando a divisão por potências de 2.
fonte
A good optimizing compiler will substitute shifts for multiplications when possible.
O que? Os turnos de bits são ordens de magnitude mais rápidas quando se trata das operações de baixo nível de uma CPU, um bom compilador de otimização faria exatamente o oposto, ou seja, transformar multiplicações comuns por potências de dois em turnos de bits.Digamos que temos um único byte:
A aplicação de um único deslocamento de bits à esquerda nos leva a:
O zero mais à esquerda foi deslocado para fora do byte e um novo zero foi anexado à extremidade direita do byte.
Os bits não rolam; eles são descartados. Isso significa que, se você saiu do turno 1101100 e depois do turno à direita, não obterá o mesmo resultado.
Deslocamento para a esquerda por N é equivalente a multiplicar por 2 N .
Deslocar para a direita por N é (se você estiver usando o complemento de um ) é o equivalente a dividir por 2 N e arredondar para zero.
O deslocamento de bits pode ser usado para multiplicação e divisão incrivelmente rápidas, desde que você esteja trabalhando com uma potência de 2. Quase todas as rotinas gráficas de baixo nível usam deslocamento de bits.
Por exemplo, no passado, usamos o modo 13h (320x200 256 cores) para jogos. No modo 13h, a memória de vídeo foi organizada sequencialmente por pixel. Para calcular a localização de um pixel, você usaria a seguinte matemática:
Agora, naquele dia e idade, a velocidade era crítica, então usamos turnos de bits para fazer essa operação.
No entanto, 320 não é um poder de dois, então, para contornar isso, precisamos descobrir o que é um poder de dois que somados faz 320:
Agora podemos converter isso em turnos à esquerda:
Para um resultado final de:
Agora temos o mesmo deslocamento de antes, exceto que, em vez de uma operação cara de multiplicação, usamos os dois turnos de bits ... em x86 seria algo parecido com isto (note, já faz uma eternidade desde que eu fiz a montagem (nota do editor: corrigida alguns erros e adicionou um exemplo de 32 bits)):
Total: 28 ciclos em qualquer CPU antiga que tivesse esses tempos.
Vrs
12 ciclos na mesma CPU antiga.
Sim, trabalharíamos muito para reduzir 16 ciclos de CPU.
No modo de 32 ou 64 bits, ambas as versões ficam muito mais curtas e rápidas. As CPUs modernas de execução fora de ordem, como o Intel Skylake (consulte http://agner.org/optimize/ ), têm uma multiplicação muito rápida de hardware (baixa latência e alta taxa de transferência); portanto, o ganho é muito menor. A família AMD Bulldozer é um pouco mais lenta, especialmente para a multiplicação de 64 bits. Nos processadores Intel e AMD Ryzen, duas mudanças são uma latência ligeiramente menor, mas mais instruções do que uma multiplicação (que pode levar a uma taxa de transferência menor):
vs.
Os compiladores farão isso por você: Veja como o GCC, Clang e Microsoft Visual C ++ usam shift + lea ao otimizar
return 320*row + col;
.O mais interessante a ser observado aqui é que o x86 possui uma instrução shift-and-add (
LEA
) que pode fazer pequenos turnos à esquerda e adicionar ao mesmo tempo, com o desempenho como umaadd
instrução. O ARM é ainda mais poderoso: um operando de qualquer instrução pode ser deslocado para a esquerda ou para a direita gratuitamente. Portanto, o dimensionamento por uma constante em tempo de compilação que é conhecida como uma potência de 2 pode ser ainda mais eficiente do que uma multiplicação.OK, nos dias modernos ... algo mais útil agora seria usar o deslocamento de bits para armazenar dois valores de 8 bits em um número inteiro de 16 bits. Por exemplo, em C #:
No C ++, os compiladores devem fazer isso por você se você usou um
struct
com dois membros de 8 bits, mas na prática eles nem sempre.fonte
c=4*d
, terá uma mudança. Se você escreverk = (n<0)
isso também pode ser feito com turnos:k = (n>>31)&1
para evitar um galho. Resumindo, esse aprimoramento na inteligência dos compiladores significa que agora não é necessário usar esses truques no código C, e eles comprometem a legibilidade e a portabilidade. Ainda é muito bom conhecê-los se você estiver escrevendo, por exemplo, código vetorial SSE; ou qualquer situação em que você precise rápido e há um truque que o compilador não está usando (por exemplo, código da GPU).if(x >= 1 && x <= 9)
que pode ser feito, poisif( (unsigned)(x-1) <=(unsigned)(9-1))
alterar dois testes condicionais para um pode ser uma grande vantagem de velocidade; especialmente quando permite execução predicada em vez de ramificações. Eu usei isso por anos (quando justificado) até notar há 10 anos que os compiladores começaram a fazer essa transformação no otimizador e parei. Ainda é bom saber, pois existem situações semelhantes em que o compilador não pode fazer a transformação para você. Ou se você estiver trabalhando em um compilador.Operações bit a bit, incluindo troca de bits, são fundamentais para hardware de baixo nível ou programação incorporada. Se você ler uma especificação para um dispositivo ou até mesmo alguns formatos de arquivos binários, verá bytes, palavras e dwords divididos em campos de bits alinhados que não são de bytes, que contêm vários valores de interesse. O acesso a esses campos de bits para leitura / gravação é o uso mais comum.
Um exemplo real simples de programação gráfica é que um pixel de 16 bits é representado da seguinte maneira:
Para obter o valor verde, você faria o seguinte:
Explicação
Para obter o valor de ONLY verde, que começa no deslocamento 5 e termina em 10 (ou seja, 6 bits de comprimento), você precisa usar uma máscara (de bits) que, quando aplicada contra todo o pixel de 16 bits, produzirá apenas os bits em que estamos interessados.
A máscara apropriada é 0x7E0, que em binário é 0000011111100000 (que é 2016 em decimal).
Para aplicar uma máscara, use o operador AND (&).
Depois de aplicar a máscara, você terá um número de 16 bits, que na verdade é apenas um número de 11 bits, já que o MSB está no 11º bit. O verde tem, na verdade, apenas 6 bits de comprimento, então precisamos reduzi-lo usando o deslocamento à direita (11 - 6 = 5), daí o uso de 5 como deslocamento (
#define GREEN_OFFSET 5
).Também é comum o uso de deslocamento de bits para multiplicação e divisão rápidas por potências de 2:
fonte
Mascaramento e mudança de bits
A troca de bits é frequentemente usada na programação gráfica de baixo nível. Por exemplo, um determinado valor de cor de pixel codificado em uma palavra de 32 bits.
Para uma melhor compreensão, o mesmo valor binário rotulado com quais seções representam qual parte da cor.
Digamos, por exemplo, que queremos obter o valor verde da cor desse pixel. Podemos facilmente obter esse valor mascarando e mudando .
Nossa máscara:
O
&
operador lógico garante que apenas os valores em que a máscara seja 1 sejam mantidos. A última coisa que precisamos fazer agora é obter o valor inteiro correto deslocando todos esses bits para a direita em 16 lugares (deslocamento lógico para a direita) .E pronto, temos o número inteiro representando a quantidade de verde na cor do pixel:
Isto é frequentemente usado para a codificação ou decodificação de formatos de imagem, como
jpg
,png
, etc.fonte
Um problema é que o seguinte depende da implementação (de acordo com o padrão ANSI):
x pode agora ser 127 (01111111) ou ainda -1 (11111111).
Na prática, geralmente é o último.
fonte
Estou escrevendo apenas dicas e truques. Pode ser útil em testes e exames.
n = n*2
:n = n<<1
n = n/2
:n = n>>1
!(n & (n-1))
n
:n |= (1 << x)
x&1 == 0
(par)x ^ (1<<n)
fonte
Observe que na implementação Java, o número de bits a serem alterados é modificado pelo tamanho da fonte.
Por exemplo:
é igual a 2. Você pode esperar que mudar os bits para a direita 65 vezes zere tudo, mas na verdade é o equivalente a:
Isso vale para <<, >> e >>>. Eu não tentei em outros idiomas.
fonte
gcc 5.4.0
dá um aviso, mas dá2
5 >> 65; também.Algumas operações / manipulações de bits úteis em Python.
Eu implementei a resposta de Ravi Prakash em Python.
fonte
Esteja ciente de que apenas a versão de 32 bits do PHP está disponível na plataforma Windows.
Então, se você, por exemplo, mudar << ou >> mais do que 31 bits, os resultados serão inesperados. Normalmente, o número original em vez de zeros será retornado, e pode ser um bug muito complicado.
Obviamente, se você usa a versão de 64 bits do PHP (Unix), deve evitar mudar mais de 63 bits. No entanto, por exemplo, o MySQL usa o BIGINT de 64 bits, portanto não deve haver nenhum problema de compatibilidade.
ATUALIZAÇÃO: No PHP 7 Windows, as compilações PHP finalmente podem usar números inteiros de 64 bits: O tamanho de um número inteiro depende da plataforma, embora um valor máximo de cerca de dois bilhões seja o valor usual (que é assinado em 32 bits). As plataformas de 64 bits geralmente têm um valor máximo de cerca de 9E18, exceto no Windows anterior ao PHP 7, onde sempre eram 32 bits.
fonte