Estou procurando uma maneira eficiente de determinar a posição do bit menos significativo definido em um número inteiro, por exemplo, para 0x0FF0 seria 4.
Uma implementação trivial é esta:
unsigned GetLowestBitPos(unsigned value)
{
assert(value != 0); // handled separately
unsigned pos = 0;
while (!(value & 1))
{
value >>= 1;
++pos;
}
return pos;
}
Alguma idéia de como tirar alguns ciclos disso?
(Nota: esta pergunta é para pessoas que gostam dessas coisas, não para pessoas me dizerem que a otimização xyz é má.)
[editar] Obrigado a todos pelas ideias! Aprendi algumas outras coisas também. Legal!
c++
c
optimization
bit-manipulation
Peterchen
fonte
fonte
Respostas:
Bit Twiddling Hacks oferece uma excelente coleção de, er, bit twiddling hacks, com discussão de desempenho / otimização anexada. Minha solução favorita para o seu problema (desse site) é «multiplique e procure»:
Referências úteis:
fonte
__builtin_ffsl
ouffsl
?Por que não usar o ffs integrado ? (Eu peguei uma página de manual do Linux, mas está mais amplamente disponível do que isso.)
fonte
Há uma instrução assembly x86 (
bsf
) que fará isso. :)Mais otimizado ?!
Nota:
A otimização nesse nível é inerentemente dependente da arquitetura. Os processadores de hoje são muito complexos (em termos de previsão de branch, perda de cache, pipelining) que é tão difícil prever qual código é executado mais rapidamente em qual arquitetura. Diminuir as operações de 32 para 9 ou coisas assim pode até diminuir o desempenho em algumas arquiteturas. O código otimizado em uma única arquitetura pode resultar em um código pior na outra. Acho que você otimizaria isso para uma CPU específica ou deixaria como está e deixaria o compilador escolher o que acha que é melhor.
fonte
A maioria das arquiteturas modernas terá alguma instrução para encontrar a posição do bit do conjunto mais baixo, ou do bit do conjunto mais alto, ou contar o número de zeros à esquerda, etc.
Se você tiver qualquer uma das instruções desta classe, poderá emular as outras por um custo baixo.
Reserve um momento para trabalhar com isso no papel e perceber que
x & (x-1)
limpará o bit mais baixo definido em x e( x & ~(x-1) )
retornará apenas o bit mais baixo definido, independentemente da arquitetura, comprimento da palavra etc. Sabendo disso, é trivial usar contagem de hardware -zeroes / bit do conjunto mais alto para encontrar o bit do conjunto mais baixo se não houver instrução explícita para fazê-lo.Se não houver suporte de hardware relevante, a implementação de multiplicação e pesquisa de zeros à esquerda fornecida aqui ou um dos na página Bit Twiddling Hacks pode ser convertida trivialmente para fornecer o bit de conjunto mais baixo usando as identidades acima e tem a vantagem de não ter ramificações.
fonte
Weee, muitas soluções e nenhum benchmark à vista. Vocês deveriam ter vergonha de si mesmos ;-)
Minha máquina é um Intel i530 (2,9 GHz), executando o Windows 7 de 64 bits. Compilei com uma versão de 32 bits do MinGW.
Meu código:
fonte
BSF
Tem uma falsa dependência em sua saída (desde o comportamento real quando input = 0 deve deixar a saída inalterada). gcc infelizmente transforma isso em uma dependência carregada por loop ao não limpar o registro entre as iterações do loop. Portanto, o loop deve ser executado em um a cada 5 ciclos, com gargalo em BSF (3) + CMOV (2) latência.ffs()
deveria ter tido uma taxa de transferência de um por clock (3 uops, 1 para BSF e 2 para CMOV, e eles podem ser executados em portas diferentes). Com a mesma sobrecarga de loop, são 7 uops ALU que podem ser executados (em sua CPU) a 3 por clock. Sobrecarga domina! Fonte: agner.org/optimizebsf ecx, [ebx+edx*4]
não for tratadaecx
como uma entrada que deve ser aguardada. (ECX foi escrito pela última vez pelo CMOV da iteração anterior). Mas a CPU se comporta dessa forma, para implementar o comportamento "deixe o destino inalterado se a fonte for zero" (portanto, não é realmente um falso dep como é para TZCNT; uma dependência de dados é necessária porque não há ramificação + execução especulativa na suposição que a entrada é diferente de zero). Poderíamos superar isso adicionando umxor ecx,ecx
antes debsf
, para quebrar a dependência do ECX.A solução mais rápida (não intrínseca / não montadora) para isso é encontrar o byte mais baixo e usar esse byte em uma tabela de consulta de 256 entradas. Isso dá a você um desempenho de pior caso de quatro instruções condicionais e um melhor caso de 1. Esta não é apenas a menor quantidade de instruções, mas a menor quantidade de ramificações, o que é superimportante no hardware moderno.
Sua tabela (256 entradas de 8 bits) deve conter o índice do LSB para cada número no intervalo 0-255. Você verifica cada byte de seu valor e encontra o byte diferente de zero mais baixo e, em seguida, usa esse valor para pesquisar o índice real.
Isso requer 256 bytes de memória, mas se a velocidade desta função é tão importante, então 256 bytes vale a pena,
Por exemplo
fonte
OMG acabou de entrar em espiral.
O que falta na maioria desses exemplos é um pouco de compreensão sobre como todo o hardware funciona.
Sempre que você tem um branch, a CPU tem que adivinhar qual branch será usado. O canal de instrução é carregado com as instruções que conduzem ao caminho adivinhado. Se a CPU adivinhou errado, o pipe de instrução é liberado e o outro branch deve ser carregado.
Considere o simples loop while no topo. A suposição será permanecer dentro do loop. Estará errado pelo menos uma vez quando sair do loop. Isso irá limpar o tubo de instrução. Esse comportamento é um pouco melhor do que supor que ele sairá do loop, caso em que esvaziaria o canal de instrução a cada iteração.
A quantidade de ciclos de CPU perdidos varia muito de um tipo de processador para outro. Mas você pode esperar entre 20 e 150 ciclos de CPU perdidos.
O próximo pior grupo é aquele em que você pensa que salvará algumas iterações, dividindo o valor em partes menores e adicionando mais ramificações. Cada uma dessas ramificações adiciona uma oportunidade adicional para limpar o canal de instrução e custar outros 20 a 150 ciclos de clock.
Vamos considerar o que acontece quando você procura um valor em uma tabela. Provavelmente, o valor não está no cache, pelo menos não na primeira vez que sua função é chamada. Isso significa que a CPU fica paralisada enquanto o valor é carregado do cache. Novamente, isso varia de uma máquina para outra. Os novos chips da Intel na verdade usam isso como uma oportunidade para trocar threads enquanto a thread atual aguarda a conclusão do carregamento do cache. Isso pode ser facilmente mais caro do que uma descarga de tubo de instrução; no entanto, se você estiver executando esta operação várias vezes, é provável que ocorra apenas uma vez.
Claramente, a solução de tempo constante mais rápida é aquela que envolve matemática determinística. Uma solução pura e elegante.
Minhas desculpas se isso já foi coberto.
Todo compilador que eu uso, exceto XCODE AFAIK, tem intrínsecos de compilador tanto para a varredura de bits direta quanto para a varredura de bits reversa. Eles compilarão em uma única instrução de montagem na maioria dos hardwares sem perda de cache, previsão de perda de ramificação e nenhum outro programador gerou obstáculos.
Para compiladores Microsoft, use _BitScanForward & _BitScanReverse.
Para GCC, use __builtin_ffs, __builtin_clz, __builtin_ctz.
Além disso, evite postar uma resposta e potencialmente enganar os recém-chegados se você não tiver conhecimento adequado sobre o assunto em discussão.
Desculpe, esqueci totalmente de fornecer uma solução .. Este é o código que uso no IPAD, que não tem instruções de nível de montagem para a tarefa:
O que devemos entender aqui é que não é a comparação que é cara, mas o branch que ocorre após a comparação. A comparação, neste caso, é forçada a um valor de 0 ou 1 com .. == 0, e o resultado é usado para combinar a matemática que ocorreria em qualquer um dos lados do galho.
Editar:
O código acima está totalmente quebrado. Este código funciona e ainda não tem ramificações (se otimizado):
Isso retorna -1 se for dado 0. Se você não se importa com 0 ou está feliz em obter 31 para 0, remova o cálculo de i0, economizando um pedaço de tempo.
fonte
-O3
godbolt.org/z/gcsUHdInspirado por esta postagem semelhante que envolve a busca por um determinado bit, ofereço o seguinte:
Prós:
Contras:
Atualização: conforme apontado nos comentários, uma união é uma implementação mais limpa (para C, pelo menos) e se pareceria com:
Isso pressupõe ints de 32 bits com armazenamento little-endian para tudo (pense em processadores x86).
fonte
int
éint32_t
, e que deslocamento para a direita assinado é um deslocamento aritmético (em C ++ a sua implementação-definido)Isso pode ser feito com o pior caso de menos de 32 operações:
Princípio: verificar 2 ou mais bits é tão eficiente quanto verificar 1 bit.
Portanto, por exemplo, não há nada que o impeça de verificar em qual agrupamento está primeiro e, em seguida, verificar cada bit do menor ao maior nesse grupo.
Então ...
se você verificar 2 bits por vez, terá no pior caso (Nbits / 2) + 1 verificações no total.
se você verificar 3 bits por vez, terá no pior caso (Nbits / 3) + 2 verificações no total.
...
O ideal seria verificar em grupos de 4. O que exigiria no pior caso 11 operações em vez de 32.
O melhor caso vai de 1 verificação de seus algoritmos a 2 verificações se você usar essa ideia de agrupamento. Mas aquele 1 cheque extra no melhor dos casos vale a pena para as economias do pior caso.
Nota: Eu escrevo por completo em vez de usar um loop porque é mais eficiente dessa forma.
fonte
Por que não usar a pesquisa binária ? Isso sempre será concluído após 5 operações (assumindo um tamanho interno de 4 bytes):
fonte
Outro método (divisão do módulo e pesquisa) merece uma menção especial aqui do mesmo link fornecido por @ anton-tykhyy. este método é muito semelhante em desempenho ao método DeBruijn multiplicação e pesquisa, com uma ligeira mas importante diferença.
divisão de módulo e pesquisa
a divisão do módulo e o método de pesquisa retornam valores diferentes para v = 0x00000000 ev = FFFFFFFF, enquanto o método DeBruijn multiply e lookup retorna zero em ambas as entradas.
teste:-
fonte
mod
é lento. Em vez disso, você pode usar o método original de multiplicação e pesquisa e subtrair!v
der
para lidar com os casos extremos.De acordo com a página Chess Programming BitScan e minhas próprias medidas, subtrair e xor é mais rápido do que negar e mascarar.
(Observe que se você for contar os zeros à direita
0
, o método como eu o fiz retorna,63
enquanto o negate e a máscara retornam0
.)Aqui está um subtrair e xor de 64 bits:
Para referência, aqui está uma versão de 64 bits do método negate e mask:
fonte
(v ^ (v-1))
funciona fornecidov != 0
. No caso dev == 0
retornar 0xFF .... FF enquanto(v & -v)
dá zero (que por sinal também está errado, buf pelo menos leva a um resultado razoável).v ^ (v-1)
, então não há como diferenciá-los. No meu cenário, zero nunca será inserido.Você pode verificar se algum dos bits de ordem inferior está definido. Nesse caso, observe a ordem inferior dos bits restantes. por exemplo,:
32 bits int - verifique se algum dos primeiros 16 está definido. Nesse caso, verifique se algum dos 8 primeiros está definido. se então, ....
caso contrário, verifique se algum dos 16 superiores estão definidos.
Essencialmente, é uma pesquisa binária.
fonte
Veja minha resposta aqui para saber como fazer isso com uma única instrução x86, exceto que, para encontrar o conjunto de bits menos significativo, você desejará a
BSF
instrução ("varredura de bits para frente") em vez daBSR
descrita lá.fonte
Outra solução, possivelmente não a mais rápida, mas parece muito boa.
Pelo menos não tem ramos. ;)
fonte
1
s do menos significativo 1 a LSB, o uso((x & -x) - 1) << 1
em vezx ^ (x-1)
50% de todos os números retornarão na primeira linha do código.
75% de todos os números retornarão nas primeiras 2 linhas de código.
87% de todos os números retornarão nas primeiras 3 linhas do código.
94% de todos os números retornarão nas primeiras 4 linhas do código.
97% de todos os números retornarão nas primeiras 5 linhas de código.
etc.
Acho que as pessoas que estão reclamando de quão ineficiente é o pior cenário para este código não entendem o quão raro essa condição acontecerá.
fonte
Encontrei este truque inteligente usando 'máscaras mágicas' em "A arte da programação, parte 4", que o faz em tempo O (log (n)) para números de n bits. [com log (n) espaço extra]. A verificação de soluções típicas para o bit definido é O (n) ou precisa de O (n) espaço extra para uma tabela de consulta, portanto, esse é um bom compromisso.
Máscaras mágicas:
Ideia-chave: Nº de zeros à direita em x = 1 * [(x & m0) = 0] + 2 * [(x & m1) = 0] + 4 * [(x & m2) = 0] + ...
fonte
Se C ++ 11 está disponível para você, às vezes um compilador pode fazer a tarefa para você :)
O resultado é um índice baseado em 1.
fonte
ffs()
em tempo de compilação, portanto, você não precisa usar isso para que a propagação de constante funcione. (Você tem que evitar inline-asm, é claro.) Se você realmente precisa fazer algo que funciona como um C ++ 11constexpr
, você ainda pode usar o GNU C__builtin_ffs
.Isso é em relação à resposta de @Anton Tykhyy
Aqui está minha implementação constexpr C ++ 11 eliminando castts e removendo um aviso no VC ++ 17 truncando um resultado de 64 bits para 32 bits:
Para contornar o problema de 0 x 1 e 0 x 0, ambos retornando 0, você pode fazer:
mas se o compilador não puder ou não quiser pré-processar a chamada, ele adicionará alguns ciclos ao cálculo.
Finalmente, se estiver interessado, aqui está uma lista de afirmações estáticas para verificar se o código faz o que se destina a:
fonte
Aqui está uma alternativa simples, embora encontrar registros seja um pouco caro.
fonte
Recentemente, vi que o primeiro-ministro de Singapura postou um programa que ele escreveu no Facebook, há uma linha para mencioná-lo.
A lógica é simplesmente "valor & -valor", suponha que você tenha 0x0FF0, então, 0FF0 & (F00F + 1), que é igual a 0x0010, o que significa que o menor 1 está no 4º bit .. :)
fonte
Se você tiver os recursos, pode sacrificar a memória para melhorar a velocidade:
Nota: Esta tabela consumiria pelo menos 4 GB (16 GB se deixarmos o tipo de retorno como
unsigned
). Este é um exemplo de troca de um recurso limitado (RAM) por outro (velocidade de execução).Se sua função precisa permanecer portátil e funcionar o mais rápido possível a qualquer custo, este é o caminho a percorrer. Na maioria dos aplicativos do mundo real, uma tabela de 4 GB não é realista.
fonte
:)
@Dan: Você está correto sobre o cache de memória. Veja o comentário de Mikeage acima.