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.