Posição do bit menos significativo que está definido


120

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!


while ((valor _N >> (++ pos))! = 0);
Thomas

Respostas:


170

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»:

unsigned int v;  // find the number of trailing zeros in 32-bit v 
int r;           // result goes here
static const int MultiplyDeBruijnBitPosition[32] = 
{
  0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, 
  31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9
};
r = MultiplyDeBruijnBitPosition[((uint32_t)((v & -v) * 0x077CB531U)) >> 27];

Referências úteis:


18
Por que o downvote? Esta é possivelmente a implementação mais rápida, dependendo da velocidade da multiplicação. Certamente é um código compacto, e o truque (v & -v) é algo que todos deveriam aprender e lembrar.
Adam Davis,

2
+1 muito legal, quão cara é uma operação de multiplicação comparada a uma operação if (X&Y)?
Brian R. Bondy,

4
Alguém sabe como o desempenho deste se compara ao __builtin_ffslou ffsl?
Steven Lu

2
@Jim Balter, mas o módulo é muito lento em comparação com a multiplicação no hardware moderno. Então, eu não diria que é uma solução melhor.
Apriori,

2
Parece-me que ambos os valores 0x01 e 0x00 resultam no valor 0 da matriz. Aparentemente, esse truque indicará que o bit mais baixo é definido se 0 for passado!
abelenky

80

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.)

ffs (3) - Página man do Linux

Nome

ffs - encontra o primeiro conjunto de bits em uma palavra

Sinopse

#include <strings.h>
int ffs(int i);
#define _GNU_SOURCE
#include <string.h>
int ffsl(long int i);
int ffsll(long long int i);

Descrição

A função ffs () retorna a posição do primeiro bit (menos significativo) definido na palavra i. O bit menos significativo é a posição 1 e a posição mais significativa, por exemplo, 32 ou 64. As funções ffsll () e ffsl () fazem o mesmo, mas levam argumentos de tamanhos possivelmente diferentes.

Valor de retorno

Essas funções retornam a posição do primeiro conjunto de bits, ou 0 se nenhum bit for definido em i.

De acordo com

4.3BSD, POSIX.1-2001.

Notas

Os sistemas BSD possuem um protótipo em <string.h>.


6
FYI, isso é compilado para o comando de montagem correspondente, quando disponível.
Jérémie

46

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.


20
@dwc: Eu entendo, mas acho que esta cláusula: "Alguma idéia de como tirar alguns ciclos disso?" torna essa resposta perfeitamente aceitável!
Mehrdad Afshari

5
+1 Sua resposta depende necessariamente de sua arquitetura por causa do endianismo, portanto, recorrer às instruções de montagem é uma resposta perfeitamente válida.
Chris Lutz,

3
+1 resposta inteligente, sim, não é C ou C ++, mas é a ferramenta certa para o trabalho.
Andrew Hare,

1
Espere, esquece. O valor real do inteiro não importa aqui. Desculpe.
Chris Lutz,

2
@Bastian: Eles definem ZF = 1 se o operando for zero.
Mehrdad Afshari

43

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.


18

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.

$ gcc --version
gcc.exe (GCC) 4.7.2

$ gcc bench.c -o bench.exe -std=c99 -Wall -O2
$ bench
Naive loop.         Time = 2.91  (Original questioner)
De Bruijn multiply. Time = 1.16  (Tykhyy)
Lookup table.       Time = 0.36  (Andrew Grant)
FFS instruction.    Time = 0.90  (ephemient)
Branch free mask.   Time = 3.48  (Dan / Jim Balter)
Double hack.        Time = 3.41  (DocMax)

$ gcc bench.c -o bench.exe -std=c99 -Wall -O2 -march=native
$ bench
Naive loop.         Time = 2.92
De Bruijn multiply. Time = 0.47
Lookup table.       Time = 0.35
FFS instruction.    Time = 0.68
Branch free mask.   Time = 3.49
Double hack.        Time = 0.92

Meu código:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>


#define ARRAY_SIZE 65536
#define NUM_ITERS 5000  // Number of times to process array


int find_first_bits_naive_loop(unsigned nums[ARRAY_SIZE])
{
    int total = 0; // Prevent compiler from optimizing out the code
    for (int j = 0; j < NUM_ITERS; j++) {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            unsigned value = nums[i];
            if (value == 0)
                continue;
            unsigned pos = 0;
            while (!(value & 1))
            {
                value >>= 1;
                ++pos;
            }
            total += pos + 1;
        }
    }

    return total;
}


int find_first_bits_de_bruijn(unsigned nums[ARRAY_SIZE])
{
    static const int MultiplyDeBruijnBitPosition[32] = 
    {
       1, 2, 29, 3, 30, 15, 25, 4, 31, 23, 21, 16, 26, 18, 5, 9, 
       32, 28, 14, 24, 22, 20, 17, 8, 27, 13, 19, 7, 12, 6, 11, 10
    };

    int total = 0; // Prevent compiler from optimizing out the code
    for (int j = 0; j < NUM_ITERS; j++) {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            unsigned int c = nums[i];
            total += MultiplyDeBruijnBitPosition[((unsigned)((c & -c) * 0x077CB531U)) >> 27];
        }
    }

    return total;
}


unsigned char lowestBitTable[256];
int get_lowest_set_bit(unsigned num) {
    unsigned mask = 1;
    for (int cnt = 1; cnt <= 32; cnt++, mask <<= 1) {
        if (num & mask) {
            return cnt;
        }
    }

    return 0;
}
int find_first_bits_lookup_table(unsigned nums[ARRAY_SIZE])
{
    int total = 0; // Prevent compiler from optimizing out the code
    for (int j = 0; j < NUM_ITERS; j++) {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            unsigned int value = nums[i];
            // note that order to check indices will depend whether you are on a big 
            // or little endian machine. This is for little-endian
            unsigned char *bytes = (unsigned char *)&value;
            if (bytes[0])
                total += lowestBitTable[bytes[0]];
            else if (bytes[1])
              total += lowestBitTable[bytes[1]] + 8;
            else if (bytes[2])
              total += lowestBitTable[bytes[2]] + 16;
            else
              total += lowestBitTable[bytes[3]] + 24;
        }
    }

    return total;
}


int find_first_bits_ffs_instruction(unsigned nums[ARRAY_SIZE])
{
    int total = 0; // Prevent compiler from optimizing out the code
    for (int j = 0; j < NUM_ITERS; j++) {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            total +=  __builtin_ffs(nums[i]);
        }
    }

    return total;
}


int find_first_bits_branch_free_mask(unsigned nums[ARRAY_SIZE])
{
    int total = 0; // Prevent compiler from optimizing out the code
    for (int j = 0; j < NUM_ITERS; j++) {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            unsigned value = nums[i];
            int i16 = !(value & 0xffff) << 4;
            value >>= i16;

            int i8 = !(value & 0xff) << 3;
            value >>= i8;

            int i4 = !(value & 0xf) << 2;
            value >>= i4;

            int i2 = !(value & 0x3) << 1;
            value >>= i2;

            int i1 = !(value & 0x1);

            int i0 = (value >> i1) & 1? 0 : -32;

            total += i16 + i8 + i4 + i2 + i1 + i0 + 1;
        }
    }

    return total;
}


int find_first_bits_double_hack(unsigned nums[ARRAY_SIZE])
{
    int total = 0; // Prevent compiler from optimizing out the code
    for (int j = 0; j < NUM_ITERS; j++) {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            unsigned value = nums[i];
            double d = value ^ (value - !!value); 
            total += (((int*)&d)[1]>>20)-1022; 
        }
    }

    return total;
}


int main() {
    unsigned nums[ARRAY_SIZE];
    for (int i = 0; i < ARRAY_SIZE; i++) {
        nums[i] = rand() + (rand() << 15);
    }

    for (int i = 0; i < 256; i++) {
        lowestBitTable[i] = get_lowest_set_bit(i);
    }


    clock_t start_time, end_time;
    int result;

    start_time = clock();
    result = find_first_bits_naive_loop(nums);
    end_time = clock();
    printf("Naive loop.         Time = %.2f, result = %d\n", 
        (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);

    start_time = clock();
    result = find_first_bits_de_bruijn(nums);
    end_time = clock();
    printf("De Bruijn multiply. Time = %.2f, result = %d\n", 
        (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);

    start_time = clock();
    result = find_first_bits_lookup_table(nums);
    end_time = clock();
    printf("Lookup table.       Time = %.2f, result = %d\n", 
        (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);

    start_time = clock();
    result = find_first_bits_ffs_instruction(nums);
    end_time = clock();
    printf("FFS instruction.    Time = %.2f, result = %d\n", 
        (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);

    start_time = clock();
    result = find_first_bits_branch_free_mask(nums);
    end_time = clock();
    printf("Branch free mask.   Time = %.2f, result = %d\n", 
        (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);

    start_time = clock();
    result = find_first_bits_double_hack(nums);
    end_time = clock();
    printf("Double hack.        Time = %.2f, result = %d\n", 
        (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);
}

8
Os benchmarks para ambos de Bruijn e lookup podem ser enganosos - sentados em um loop apertado como aquele, após a primeira operação as tabelas de lookup para cada tipo serão fixadas no cache L1 até depois do último loop. Não é provável que corresponda ao uso no mundo real.
MattW

1
Para as entradas com um zero no byte inferior, ele obtém os bytes mais altos armazenando / recarregando em vez de deslocar, por causa do lançamento do ponteiro. (BTW totalmente desnecessário, e o torna dependente de endian, ao contrário de um turno não faria). De qualquer forma, o microbenchmark não é apenas irreal devido ao cache quente, mas também tem os preditores de branch preparados e as entradas de teste que prevêem muito bem e fazem o LUT trabalhar menos. Muitos casos de uso reais têm uma distribuição mais uniforme de resultados, não de entradas.
Peter Cordes

2
Seu loop FFS infelizmente é desacelerado por uma falsa dependência na instrução BSF que seu compilador antigo não evita ( mas o gcc mais novo deveria, o mesmo para popcnt / lzcnt / tzcnt . BSFTem 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.
Peter Cordes

1
Seu benchmark descobriu que o LUT tem quase exatamente o dobro da taxa de transferência do método FFS, o que corresponde extremamente bem à minha previsão de análise estática :). Observe que você está medindo a taxa de transferência, não a latência, porque a única dependência serial em seu loop é a soma do total. Sem a falsa dependê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/optimize
Peter Cordes

1
Sim, a execução fora de ordem pode sobrepor várias iterações do loop se bsf ecx, [ebx+edx*4]não for tratada ecxcomo 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 um xor ecx,ecxantes de bsf, para quebrar a dependência do ECX.
Peter Cordes

17

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

byte lowestBitTable[256] = {
.... // left as an exercise for the reader to generate
};

unsigned GetLowestBitPos(unsigned value)
{
  // note that order to check indices will depend whether you are on a big 
  // or little endian machine. This is for little-endian
  byte* bytes = (byte*)value;
  if (bytes[0])
    return lowestBitTable[bytes[0]];
  else if (bytes[1])
      return lowestBitTable[bytes[1]] + 8;
  else if (bytes[2])
      return lowestBitTable[bytes[2]] + 16;
  else
      return lowestBitTable[bytes[3]] + 24;  
}

1
Na verdade, é o pior caso de três condicionais :) Mas sim, esta é a abordagem mais rápida (e geralmente o que as pessoas procuram em perguntas de entrevista como esta).
Brian,

4
Você não quer um +8, +16, +24 aí em algum lugar?
Mark Ransom,

7
Qualquer tabela de pesquisa aumenta a chance de perda de cache e pode incorrer no custo de acesso à memória, que pode ser várias ordens de magnitude maior do que a execução de instruções.
Mehrdad Afshari

1
eu até usaria deslocamentos de bits (deslocando em 8 cada vez). poderia ser feito inteiramente usando registros então. usando ponteiros, você terá que acessar a memória.
Johannes Schaub - litb

1
Solução razoável, mas entre o potencial de a tabela de pesquisa não estar no cache (o que pode ser resolvido, conforme apontado) e o número de ramos (erro de previsão do ramo potencial), eu prefiro muito mais a solução de multiplicação e pesquisa (sem ramos, tabela de pesquisa menor). Claro, se você pode usar intrínseco ou assembly embutido, eles provavelmente são uma escolha melhor. Ainda assim, esta solução não é ruim.

13

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:

unsigned BitScanLow_BranchFree(unsigned value)
{
    bool bwl = (value & 0x0000ffff) == 0;
    unsigned I1 = (bwl * 15);
    value = (value >> I1) & 0x0000ffff;

    bool bbl = (value & 0x00ff00ff) == 0;
    unsigned I2 = (bbl * 7);
    value = (value >> I2) & 0x00ff00ff;

    bool bnl = (value & 0x0f0f0f0f) == 0;
    unsigned I3 = (bnl * 3);
    value = (value >> I3) & 0x0f0f0f0f;

    bool bsl = (value & 0x33333333) == 0;
    unsigned I4 = (bsl * 1);
    value = (value >> I4) & 0x33333333;

    unsigned result = value + I1 + I2 + I3 + I4 - 1;

    return result;
}

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):

int BitScanLow_BranchFree(ui value)
{
    int i16 = !(value & 0xffff) << 4;
    value >>= i16;

    int i8 = !(value & 0xff) << 3;
    value >>= i8;

    int i4 = !(value & 0xf) << 2;
    value >>= i4;

    int i2 = !(value & 0x3) << 1;
    value >>= i2;

    int i1 = !(value & 0x1);

    int i0 = (value >> i1) & 1? 0 : -32;

    return i16 + i8 + i4 + i2 + i1 + i0;
}

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.


3
Eu consertei para você. Certifique-se de testar o que você postar.
Jim Balter

5
Como você pode chamá-lo de "sem agência" quando inclui uma operadora ternária?
BoltBait

2
É um movimento condicional. Uma única instrução em linguagem Assembly que usa os dois valores possíveis como parâmetros e executa uma operação mov com base na avaliação da condicional. E, portanto, é "Branch Free". não há salto para outro endereço desconhecido ou possivelmente incorreto.
Dan,

FWIW gcc gera branches até mesmo em -O3 godbolt.org/z/gcsUHd
Qix - MONICA FOI ENGANADA

7

Inspirado por esta postagem semelhante que envolve a busca por um determinado bit, ofereço o seguinte:

unsigned GetLowestBitPos(unsigned value)
{
   double d = value ^ (value - !!value); 
   return (((int*)&d)[1]>>20)-1023; 
}

Prós:

  • sem loops
  • sem ramificação
  • corre em tempo constante
  • lida com valor = 0 retornando um resultado fora dos limites
  • apenas duas linhas de código

Contras:

  • assume pouca endianness como codificado (pode ser corrigido alterando as constantes)
  • assume que double é um float IEEE real * 8 (IEEE 754)

Atualização: conforme apontado nos comentários, uma união é uma implementação mais limpa (para C, pelo menos) e se pareceria com:

unsigned GetLowestBitPos(unsigned value)
{
    union {
        int i[2];
        double d;
    } temp = { .d = value ^ (value - !!value) };
    return (temp.i[1] >> 20) - 1023;
}

Isso pressupõe ints de 32 bits com armazenamento little-endian para tudo (pense em processadores x86).


1
Interessante - ainda estou com medo de usar duplos para aritmética de bits, mas vou manter isso em mente
peterchen

Usar frexp () pode torná-lo um pouco mais portátil
aka.nice

1
O trocadilho por ponteiro não é seguro em C ou C ++. Use memcpy em C ++, ou uma união em C. (Ou uma união em C ++ se o seu compilador garante que é seguro. Por exemplo, as extensões GNU para C ++ (suportadas por muitos compiladores) garantem que o tipo de união é seguro.)
Peter Cordes

1
O gcc mais antigo também cria um código melhor com uma união em vez de um lançamento de ponteiro: ele se move diretamente de um reg FP (xmm0) para rax (com movq) em vez de armazenar / recarregar. O gcc e o clang mais recentes usam movq para as duas formas. Consulte godbolt.org/g/x7JBiL para uma versão do sindicato. É intencional que você esteja fazendo uma mudança aritmética em 20? Suas suposições também deve lista que inté int32_t, e que deslocamento para a direita assinado é um deslocamento aritmético (em C ++ a sua implementação-definido)
Peter Cordes

1
Além disso, o Visual Studio (pelo menos 2013) também usa a abordagem test / setcc / sub. Eu gosto mais do cmp / adc.
DocMax

5

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.

int getLowestBitPos(unsigned int value)
{
    //Group 1: Bits 0-3
    if(value&0xf)
    {
        if(value&0x1)
            return 0;
        else if(value&0x2)
            return 1;
        else if(value&0x4)
            return 2;
        else
            return 3;
    }

    //Group 2: Bits 4-7
    if(value&0xf0)
    {
        if(value&0x10)
            return 4;
        else if(value&0x20)
            return 5;
        else if(value&0x40)
            return 6;
        else
            return 7;
    }

    //Group 3: Bits 8-11
    if(value&0xf00)
    {
        if(value&0x100)
            return 8;
        else if(value&0x200)
            return 9;
        else if(value&0x400)
            return 10;
        else
            return 11;
    }

    //Group 4: Bits 12-15
    if(value&0xf000)
    {
        if(value&0x1000)
            return 12;
        else if(value&0x2000)
            return 13;
        else if(value&0x4000)
            return 14;
        else
            return 15;
    }

    //Group 5: Bits 16-19
    if(value&0xf0000)
    {
        if(value&0x10000)
            return 16;
        else if(value&0x20000)
            return 17;
        else if(value&0x40000)
            return 18;
        else
            return 19;
    }

    //Group 6: Bits 20-23
    if(value&0xf00000)
    {
        if(value&0x100000)
            return 20;
        else if(value&0x200000)
            return 21;
        else if(value&0x400000)
            return 22;
        else
            return 23;
    }

    //Group 7: Bits 24-27
    if(value&0xf000000)
    {
        if(value&0x1000000)
            return 24;
        else if(value&0x2000000)
            return 25;
        else if(value&0x4000000)
            return 26;
        else
            return 27;
    }

    //Group 8: Bits 28-31
    if(value&0xf0000000)
    {
        if(value&0x10000000)
            return 28;
        else if(value&0x20000000)
            return 29;
        else if(value&0x40000000)
            return 30;
        else
            return 31;
    }

    return -1;
}

+1 de mim. Não é o mais rápido, mas é mais rápido do que o original, que era o ponto ...
Andrew Grant

@ onebyone.livejournal.com: Mesmo que houvesse um bug no código, o conceito de agrupamento é o que eu estava tentando transmitir. O exemplo de código real não importa muito e poderia ser mais compacto, mas menos eficiente.
Brian R. Bondy,

Só estou me perguntando se há uma parte realmente ruim da minha resposta, ou se as pessoas simplesmente não gostaram, eu a escrevi por completo.
Brian R. Bondy,

@ onebyone.livejournal.com: Ao comparar 2 algoritmos, você deve compará-los como são, não presumindo que um será transformado magicamente por uma fase de otimização. Eu também nunca afirmei que meu algoritmo era "mais rápido". Só que é menos operações.
Brian R. Bondy,

@ onebyone.livejournal.com: ... Não preciso fazer o perfil do código acima para saber que são menos operações. Eu posso ver isso claramente. Nunca fiz nenhuma reivindicação que exigisse criação de perfil.
Brian R. Bondy,

4

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):

if (0x0000FFFF & value) {
    if (0x000000FF & value) {
        if (0x0000000F & value) {
            if (0x00000003 & value) {
                if (0x00000001 & value) {
                    return 1;
                } else {
                    return 2;
                }
            } else {
                if (0x0000004 & value) {
                    return 3;
                } else {
                    return 4;
                }
            }
        } else { ...
    } else { ...
} else { ...

1 Isso é muito semelhante à minha resposta. O tempo de execução do melhor caso é pior do que minha sugestão, mas o tempo de execução do pior caso é melhor.
Brian R. Bondy,

2

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

 unsigned int v;  // find the number of trailing zeros in v
    int r;           // put the result in r
    static const int Mod37BitPosition[] = // map a bit value mod 37 to its position
    {
      32, 0, 1, 26, 2, 23, 27, 0, 3, 16, 24, 30, 28, 11, 0, 13, 4,
      7, 17, 0, 25, 22, 31, 15, 29, 10, 12, 6, 0, 21, 14, 9, 5,
      20, 8, 19, 18
    };
    r = Mod37BitPosition[(-v & v) % 37];

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:-

unsigned int n1=0x00000000, n2=0xFFFFFFFF;

MultiplyDeBruijnBitPosition[((unsigned int )((n1 & -n1) * 0x077CB531U)) >> 27]); /* returns 0 */
MultiplyDeBruijnBitPosition[((unsigned int )((n2 & -n2) * 0x077CB531U)) >> 27]); /* returns 0 */
Mod37BitPosition[(((-(n1) & (n1))) % 37)]); /* returns 32 */
Mod37BitPosition[(((-(n2) & (n2))) % 37)]); /* returns 0 */

1
modé lento. Em vez disso, você pode usar o método original de multiplicação e pesquisa e subtrair !vde rpara lidar com os casos extremos.
Eitan T

3
@EitanT um otimizador pode muito bem transformar esse mod em uma multiplicação rápida, como no deleite dos hackers
phuclv

2

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, 63enquanto o negate e a máscara retornam 0.)

Aqui está um subtrair e xor de 64 bits:

unsigned long v;  // find the number of trailing zeros in 64-bit v 
int r;            // result goes here
static const int MultiplyDeBruijnBitPosition[64] = 
{
  0, 47, 1, 56, 48, 27, 2, 60, 57, 49, 41, 37, 28, 16, 3, 61,
  54, 58, 35, 52, 50, 42, 21, 44, 38, 32, 29, 23, 17, 11, 4, 62,
  46, 55, 26, 59, 40, 36, 15, 53, 34, 51, 20, 43, 31, 22, 10, 45,
  25, 39, 14, 33, 19, 30, 9, 24, 13, 18, 8, 12, 7, 6, 5, 63
};
r = MultiplyDeBruijnBitPosition[((uint32_t)((v ^ (v-1)) * 0x03F79D71B4CB0A89U)) >> 58];

Para referência, aqui está uma versão de 64 bits do método negate e mask:

unsigned long v;  // find the number of trailing zeros in 64-bit v 
int r;            // result goes here
static const int MultiplyDeBruijnBitPosition[64] = 
{
  0, 1, 48, 2, 57, 49, 28, 3, 61, 58, 50, 42, 38, 29, 17, 4,
  62, 55, 59, 36, 53, 51, 43, 22, 45, 39, 33, 30, 24, 18, 12, 5,
  63, 47, 56, 27, 60, 41, 37, 16, 54, 35, 52, 21, 44, 32, 23, 11,
  46, 26, 40, 15, 34, 20, 31, 10, 25, 14, 19, 9, 13, 8, 7, 6
};
r = MultiplyDeBruijnBitPosition[((uint32_t)((v & -v) * 0x03F79D71B4CB0A89U)) >> 58];

Este (v ^ (v-1))funciona fornecido v != 0. No caso de v == 0retornar 0xFF .... FF enquanto (v & -v)dá zero (que por sinal também está errado, buf pelo menos leva a um resultado razoável).
CiaPan de

@CiaPan: É um bom ponto, vou mencioná-lo. Eu estou supondo que há um número De Bruijn diferente que resolveria isso colocando 0 no 63º índice.
jnm2

Duh, não é aí que está o problema. 0 e 0x8000000000000000 resultam em 0xFFFFFFFFFFFFFFFF depois v ^ (v-1), então não há como diferenciá-los. No meu cenário, zero nunca será inserido.
jnm2

1

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.


1

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 BSFinstrução ("varredura de bits para frente") em vez da BSRdescrita lá.


1

Outra solução, possivelmente não a mais rápida, mas parece muito boa.
Pelo menos não tem ramos. ;)

uint32 x = ...;  // 0x00000001  0x0405a0c0  0x00602000
x |= x <<  1;    // 0x00000003  0x0c0fe1c0  0x00e06000
x |= x <<  2;    // 0x0000000f  0x3c3fe7c0  0x03e1e000
x |= x <<  4;    // 0x000000ff  0xffffffc0  0x3fffe000
x |= x <<  8;    // 0x0000ffff  0xffffffc0  0xffffe000
x |= x << 16;    // 0xffffffff  0xffffffc0  0xffffe000

// now x is filled with '1' from the least significant '1' to bit 31

x = ~x;          // 0x00000000  0x0000003f  0x00001fff

// now we have 1's below the original least significant 1
// let's count them

x = x & 0x55555555 + (x >>  1) & 0x55555555;
                 // 0x00000000  0x0000002a  0x00001aaa

x = x & 0x33333333 + (x >>  2) & 0x33333333;
                 // 0x00000000  0x00000024  0x00001444

x = x & 0x0f0f0f0f + (x >>  4) & 0x0f0f0f0f;
                 // 0x00000000  0x00000006  0x00000508

x = x & 0x00ff00ff + (x >>  8) & 0x00ff00ff;
                 // 0x00000000  0x00000006  0x0000000d

x = x & 0x0000ffff + (x >> 16) & 0x0000ffff;
                 // 0x00000000  0x00000006  0x0000000d
// least sign.bit pos. was:  0           6          13

para obter todos os 1s do menos significativo 1 a LSB, o uso ((x & -x) - 1) << 1em vez
phuclv

uma maneira ainda mais rápida:x ^ (x-1)
phuclv

1
unsigned GetLowestBitPos(unsigned value)
{
    if (value & 1) return 1;
    if (value & 2) return 2;
    if (value & 4) return 3;
    if (value & 8) return 4;
    if (value & 16) return 5;
    if (value & 32) return 6;
    if (value & 64) return 7;
    if (value & 128) return 8;
    if (value & 256) return 9;
    if (value & 512) return 10;
    if (value & 1024) return 11;
    if (value & 2048) return 12;
    if (value & 4096) return 13;
    if (value & 8192) return 14;
    if (value & 16384) return 15;
    if (value & 32768) return 16;
    if (value & 65536) return 17;
    if (value & 131072) return 18;
    if (value & 262144) return 19;
    if (value & 524288) return 20;
    if (value & 1048576) return 21;
    if (value & 2097152) return 22;
    if (value & 4194304) return 23;
    if (value & 8388608) return 24;
    if (value & 16777216) return 25;
    if (value & 33554432) return 26;
    if (value & 67108864) return 27;
    if (value & 134217728) return 28;
    if (value & 268435456) return 29;
    if (value & 536870912) return 30;
    return 31;
}

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á.


3
E o pior caso de erro de previsão de 32 ramos :)

1
Isso não poderia pelo menos ser transformado em uma chave ...?
Steven Lu

"Não poderia, pelo menos, ser transformado em um interruptor ...?" Você tentou fazer isso antes de sugerir que é possível? Desde quando você pode fazer cálculos nas caixas de um switch? É uma tabela de consulta, não uma classe.
j riv

1

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:

m0 = (...............01010101)  
m1 = (...............00110011)
m2 = (...............00001111)  
m3 = (.......0000000011111111)
....

Ideia-chave: Nº de zeros à direita em x = 1 * [(x & m0) = 0] + 2 * [(x & m1) = 0] + 4 * [(x & m2) = 0] + ...

int lastSetBitPos(const uint64_t x) {
    if (x == 0)  return -1;

    //For 64 bit number, log2(64)-1, ie; 5 masks needed
    int steps = log2(sizeof(x) * 8); assert(steps == 6);
    //magic masks
    uint64_t m[] = { 0x5555555555555555, //     .... 010101
                     0x3333333333333333, //     .....110011
                     0x0f0f0f0f0f0f0f0f, //     ...00001111
                     0x00ff00ff00ff00ff, //0000000011111111 
                     0x0000ffff0000ffff, 
                     0x00000000ffffffff };

    //Firstly extract only the last set bit
    uint64_t y = x & -x;

    int trailZeros = 0, i = 0 , factor = 0;
    while (i < steps) {
        factor = ((y & m[i]) == 0 ) ? 1 : 0;
        trailZeros += factor * pow(2,i);
        ++i;
    }
    return (trailZeros+1);
}

1

Se C ++ 11 está disponível para você, às vezes um compilador pode fazer a tarefa para você :)

constexpr std::uint64_t lssb(const std::uint64_t value)
{
    return !value ? 0 : (value % 2 ? 1 : lssb(value >> 1) + 1);
}

O resultado é um índice baseado em 1.


1
Inteligente, mas compila para uma montagem catastroficamente ruim quando a entrada não é uma constante de tempo de compilação. godbolt.org/g/7ajMyT . (Um loop idiota sobre os bits com gcc ou uma chamada de função recursiva real com clang.) Gcc / clang pode avaliar 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 ++ 11 constexpr, você ainda pode usar o GNU C __builtin_ffs.
Peter Cordes

0

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:

constexpr uint32_t DeBruijnSequence[32] =
{
    0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8,
    31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9
};
constexpr uint32_t ffs ( uint32_t value )
{
    return  DeBruijnSequence[ 
        (( ( value & ( -static_cast<int32_t>(value) ) ) * 0x077CB531ULL ) & 0xFFFFFFFF)
            >> 27];
}

Para contornar o problema de 0 x 1 e 0 x 0, ambos retornando 0, você pode fazer:

constexpr uint32_t ffs ( uint32_t value )
{
    return (!value) ? 32 : DeBruijnSequence[ 
        (( ( value & ( -static_cast<int32_t>(value) ) ) * 0x077CB531ULL ) & 0xFFFFFFFF)
            >> 27];
}

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:

static_assert (ffs(0x1) == 0, "Find First Bit Set Failure.");
static_assert (ffs(0x2) == 1, "Find First Bit Set Failure.");
static_assert (ffs(0x4) == 2, "Find First Bit Set Failure.");
static_assert (ffs(0x8) == 3, "Find First Bit Set Failure.");
static_assert (ffs(0x10) == 4, "Find First Bit Set Failure.");
static_assert (ffs(0x20) == 5, "Find First Bit Set Failure.");
static_assert (ffs(0x40) == 6, "Find First Bit Set Failure.");
static_assert (ffs(0x80) == 7, "Find First Bit Set Failure.");
static_assert (ffs(0x100) == 8, "Find First Bit Set Failure.");
static_assert (ffs(0x200) == 9, "Find First Bit Set Failure.");
static_assert (ffs(0x400) == 10, "Find First Bit Set Failure.");
static_assert (ffs(0x800) == 11, "Find First Bit Set Failure.");
static_assert (ffs(0x1000) == 12, "Find First Bit Set Failure.");
static_assert (ffs(0x2000) == 13, "Find First Bit Set Failure.");
static_assert (ffs(0x4000) == 14, "Find First Bit Set Failure.");
static_assert (ffs(0x8000) == 15, "Find First Bit Set Failure.");
static_assert (ffs(0x10000) == 16, "Find First Bit Set Failure.");
static_assert (ffs(0x20000) == 17, "Find First Bit Set Failure.");
static_assert (ffs(0x40000) == 18, "Find First Bit Set Failure.");
static_assert (ffs(0x80000) == 19, "Find First Bit Set Failure.");
static_assert (ffs(0x100000) == 20, "Find First Bit Set Failure.");
static_assert (ffs(0x200000) == 21, "Find First Bit Set Failure.");
static_assert (ffs(0x400000) == 22, "Find First Bit Set Failure.");
static_assert (ffs(0x800000) == 23, "Find First Bit Set Failure.");
static_assert (ffs(0x1000000) == 24, "Find First Bit Set Failure.");
static_assert (ffs(0x2000000) == 25, "Find First Bit Set Failure.");
static_assert (ffs(0x4000000) == 26, "Find First Bit Set Failure.");
static_assert (ffs(0x8000000) == 27, "Find First Bit Set Failure.");
static_assert (ffs(0x10000000) == 28, "Find First Bit Set Failure.");
static_assert (ffs(0x20000000) == 29, "Find First Bit Set Failure.");
static_assert (ffs(0x40000000) == 30, "Find First Bit Set Failure.");
static_assert (ffs(0x80000000) == 31, "Find First Bit Set Failure.");

0

Aqui está uma alternativa simples, embora encontrar registros seja um pouco caro.

if(n == 0)
  return 0;
return log2(n & -n)+1;   //Assuming the bit index starts from 1

-3

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 .. :)


1
Isso isola o bit mais baixo, mas não fornece sua posição, que é o que esta pergunta está pedindo.
rhashimoto

Eu não acho que isso funcione para encontrar o último bit também.
yyny

valor & ~ valor é 0.
khw

opa, meus olhos estão ficando ruins. Eu confundi um sinal de menos com um til. desconsidere meu comentário
khw

-8

Se você tiver os recursos, pode sacrificar a memória para melhorar a velocidade:

static const unsigned bitPositions[MAX_INT] = { 0, 0, 1, 0, 2, /* ... */ };

unsigned GetLowestBitPos(unsigned value)
{
    assert(value != 0); // handled separately
    return bitPositions[value];
}

Nota: Esta tabela consumiria pelo menos 4 GB (16 GB se deixarmos o tipo de retorno comounsigned ). 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.


1
O intervalo da entrada já está especificado pelo tipo de parâmetro - 'unsigned' é um valor de 32 bits, então não, você não está bem.
Brian,

3
umm ... seu sistema mítico e SO tem um conceito de memória paginada? Quanto tempo isso vai custar?
Mikeage

14
Esta é uma não resposta. Sua solução é completamente irreal em TODAS as aplicações do mundo real e chamá-la de "troca" é falso. Seu sistema mítico com 16 GB de memória RAM para se dedicar a uma única função simplesmente não existe. Você também teria respondido "usar um computador quântico".
Brian,

3
Sacrificar memória pela velocidade? Uma tabela de pesquisa de mais de 4 GB nunca caberá no cache de nenhuma máquina existente, então imagino que isso seja provavelmente mais lento do que quase todas as outras respostas aqui.

1
Argh. Esta resposta horrível continua me assombrando :)@Dan: Você está correto sobre o cache de memória. Veja o comentário de Mikeage acima.
e.James
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.