Existe uma maneira elegante e rápida de testar se os bits de 1 em um inteiro estão em uma região contígua?

84

Preciso testar se as posições (de 0 a 31 para um inteiro de 32 bits) com valor de bit 1 formam uma região contígua. Por exemplo:

00111111000000000000000000000000      is contiguous
00111111000000000000000011000000      is not contiguous

Quero que este teste, ou seja, alguma função has_contiguous_one_bits(int), seja portátil.

Uma maneira óbvia é fazer um loop nas posições para encontrar o primeiro bit definido, depois o primeiro bit não definido e verificar se há mais bits definidos.

Será que existe uma maneira mais rápida? Se houver métodos rápidos para encontrar os bits de conjunto mais alto e mais baixo (mas a partir desta pergunta parece que não há nenhum portátil), então uma possível implementação é

bool has_contiguous_one_bits(int val)
{
    auto h = highest_set_bit(val);
    auto l = lowest_set_bit(val);
    return val == (((1 << (h-l+1))-1)<<l);
}

Apenas por diversão, aqui estão os primeiros 100 inteiros com bits contíguos:

0 1 2 3 4 6 7 8 12 14 15 16 24 28 30 31 32 48 56 60 62 63 64 96 112 120 124 126 127 128 192 224 240 248 252 254 255 256 384 448 480 496 504 508 510 511 512 768 896 960 992 1008 1016 1020 1022 1023 1024 1536 1792 1920 1984 2016 2032 2040 2044 2046 2047 2048 3072 3584 3840 3968 4032 4064 4080 4088 4092 4094 4095 4096 6144 7168 7680 7936 8064 8128 8160 8176 8184 8188 8190 8191 8192 12288 14336 15360 15872 16128 16256 16320

eles são (claro) da forma (1<<m)*(1<<n-1)com não-negativo me n.

Walter
fonte
4
@aafulei sim, 0x0é compacto. É mais fácil definir o oposto (não compacto): se houver dois bits definidos, há pelo menos um bit não definido entre eles.
Walter
1
@KamilCuk h>=lpela funcionalidade (implícita) de highest_set_bit()elowest_set_bit()
Walter
6
OEIS A023758
pmg
6
Esse link OEIS diz que esses números têm seus dígitos não aumentando quando em binário. Outra forma de se referir a eles seria dizer que são contíguos (ou talvez conectados). Para este matemático, "compacto" significa algo muito diferente.
Teepeemm
1
@Teepeemm Acho que um dos motivos pelos quais essa pergunta acabou em questões de rede quente é exatamente por causa do uso incorreto da palavra compacto, certamente é por isso que cliquei nela: não estava pensando muito e me perguntei como faria sentido definir compactação dessa maneira. Obviamente, isso não faz sentido.
Ninguém

Respostas:

146
static _Bool IsCompact(unsigned x)
{
    return (x & x + (x & -x)) == 0;
}

Resumidamente:

x & -xdá o bit mais baixo definido em x(ou zero sex for zero).

x + (x & -x) converte a string mais baixa de 1s consecutivos em um único 1 (ou quebra em zero).

x & x + (x & -x) limpa esses 1 bits.

(x & x + (x & -x)) == 0 testa se quaisquer outros bits 1 permanecem.

Mais longo:

-xé igual ~x+1, usando o complemento de dois, que assumimos. Depois que os bits são invertidos ~x, a adição de 1 carrega de modo que inverte os bits 1 inferiores ~xe o primeiro bit 0, mas depois pára. Portanto, os bits mais baixos de -xaté e incluindo o primeiro 1 são iguais aos bits mais baixos de x, mas todos os bits mais altos são invertidos. (Exemplo: ~1001110001100011, e adicionando 1 dá 01100100, então o mínimo 100é o mesmo, mas o alto 10011é invertido 01100.) Então x & -xnos dá o único bit que é 1 em ambos, que é o bit 1 mais baixo ( 00000100). (Se xfor zero, x & -xserá zero.)

Somando isso a xfaz com que todos os 1s consecutivos, mudando-os para 0s. Ele deixará 1 no próximo bit 0 superior (ou continuará no limite superior, deixando um total agrupado de zero) ( 10100000.)

Quando isso é AND com x, há 0s nos lugares onde os 1s foram alterados para 0s (e também onde o carry mudou de 0 para 1). Portanto, o resultado não é zero apenas se houver outro 1 bit acima.

Eric Postpischil
fonte
23
Pelo menos alguém conhece o livro Hacker's Delight. Consulte o capítulo 2-1 para obter a resposta. Mas isso já foi respondido várias vezes aqui no SO. De qualquer forma: +1
Armin Montigny
33
Espero que, se algum dia você escrever esse código em produção, inclua a explicação nos comentários;)
Polygnome
14
Isso se beneficia muito de x86 BMI1 para fazer x & -xem uma única blsiinstrução, que é 1 uop no Intel, 2 uops no AMD Zen. godbolt.org/z/5zBx-A . Mas sem o BMI1, a versão de @KevinZ é ainda mais eficiente.
Peter Cordes
3
@TommyAndersen: _Boolé uma palavra-chave padrão, de acordo com C 2018 6.4.1 1.
Eric Postpischil
1
@Walter: Hmm? Este código usa unsigned. Se você deseja realizar o teste para um complemento de dois assinado int, a maneira mais fácil é simplesmente repassá-lo para a rotina nesta resposta, deixando que o intseja convertido para unsigned. Isso dará o resultado desejado. Aplicar as operações mostradas a um assinado intdiretamente pode ser problemático, devido a problemas de estouro / transporte. (Se você quiser testar o complemento ou sinal e magnitude de uma int, isso é outro assunto, em grande parte apenas de interesse teórico atualmente.)
Eric Postpischil
29

Na verdade, não há necessidade de usar intrínsecos.

Primeiro vire todos os 0s antes do primeiro 1. Em seguida, teste se o novo valor é um número mersenne. Neste algoritmo, zero é mapeado para verdadeiro.

bool has_compact_bits( unsigned const x )
{
    // fill up the low order zeroes
    unsigned const y = x | ( x - 1 );
    // test if the 1's is one solid block
    return not ( y & ( y + 1 ) );
}

Claro, se você quiser usar intrínsecos, aqui está o método popcount:

bool has_compact_bits( unsigned const x )
{
    size_t const num_bits = CHAR_BIT * sizeof(unsigned);
    size_t const sum = __builtin_ctz(x) + __builtin_popcount(x) + __builtin_clz(z);
    return sum == num_bits;
}
KevinZ
fonte
2
A primeira versão se reduz a apenas 4 instruções se compilada com -mtbm, explorando blsfill/ blcfillinstruções. Seria a versão mais curta proposta até agora. Infelizmente, quase nenhum processador suporta essa extensão de conjunto de instruções .
Giovanni Cerretani
18

Na verdade, você não precisa contar os zeros à esquerda. Como sugerido por pmg nos comentários, explorando o fato de que os números que você está procurando são aqueles da sequência OEIS A023758 , ou seja, números da forma 2 ^ i - 2 ^ j com i> = j , você pode apenas contar os zeros à direita ( ou seja, j - 1 ), alterne esses bits no valor original (equivalente a adicionar 2 ^ j - 1 ) e, em seguida, verifique se esse valor tem a forma 2 ^ i - 1 . Com os intrínsecos GCC / clang,

bool has_compact_bits(int val) {
    if (val == 0) return true; // __builtin_ctz undefined if argument is zero
    int j = __builtin_ctz(val) + 1;
    val |= (1 << j) - 1; // add 2^j - 1
    val &= (val + 1); // val set to zero if of the form (2^i - 1)
    return val == 0;
}

Esta versão é um pouco mais rápida que a sua e a proposta por KamilCuk e a de Yuri Feldman apenas com popcount.

Se você estiver usando C ++ 20, poderá obter uma função portátil substituindo __builtin_ctzpor std::countr_zero:

#include <bit>

bool has_compact_bits(int val) {
    int j = std::countr_zero(static_cast<unsigned>(val)) + 1; // ugly cast
    val |= (1 << j) - 1; // add 2^j - 1
    val &= (val + 1); // val set to zero if of the form (2^i - 1)
    return val == 0;
}

O elenco é feio, mas avisa que é melhor trabalhar com tipos não assinados ao manipular bits. As alternativas pré-C ++ 20 sãoboost::multiprecision::lsb .

Editar:

O benchmark no link tachado foi limitado pelo fato de que nenhuma instrução popcount foi emitida para a versão de Yuri Feldman. Tentando compilá-los no meu PC com -march=westmere, medi o seguinte tempo para 1 bilhão de iterações com sequências idênticas de std::mt19937:

  • sua versão: 5,7 s
  • Segunda versão de KamilCuk: 4.7 s
  • minha versão: 4.7 s
  • Primeira versão de Eric Postpischil: 4,3 s
  • Versão de Yuri Feldman (usando explicitamente __builtin_popcount): 4.1 s

Então, pelo menos na minha arquitetura, o mais rápido parece ser aquele com popcount.

Editar 2:

Eu atualizei meu benchmark com a nova versão de Eric Postpischil. Conforme solicitado nos comentários, o código do meu teste pode ser encontrado aqui . Eu adicionei um loop autônomo para estimar o tempo necessário para o PRNG. Também adicionei as duas versões de KevinZ. O código foi compilado no clang com -O3 -msse4 -mbmipara obter popcnteblsi instrução (graças a Peter Cordes).

Resultados: pelo menos na minha arquitetura, a versão de Eric Postpischil é exatamente tão rápida quanto a de Yuri Feldman, e pelo menos duas vezes mais rápida do que qualquer outra versão proposta até agora.

Giovanni Cerretani
fonte
I removido uma operação: return (x & x + (x & -x)) == 0;.
Eric Postpischil
3
Este é um benchmarking de uma versão mais antiga da versão de @Eric, certo? Com a versão atual, Eric compila para o mínimo de instruções com gcc -O3 -march=nehalem(para disponibilizar popcnt), ou menos se BMI1 blsiestiver disponível para x & -x: godbolt.org/z/zuyj_f . E as instruções são todas simples single-uop, exceto para a popcntversão de Yuri que tem latência de 3 ciclos. (Mas suponho que você estava analisando o rendimento.) Também suponho que você deve ter removido o and valdo Yuri ou ele seria mais lento.
Peter Cordes
2
Além disso, em qual hardware você fez o benchmark? Vincular seu código de benchmark completo no Godbolt ou algo assim seria uma boa ideia, para que futuros leitores possam testar facilmente sua implementação C ++.
Peter Cordes
2
Você também deve testar a versão de @KevinZ; ele compila para ainda menos instruções sem BMI1 (pelo menos com clang; a versão não embutida do gcc desperdiça a move não consegue tirar proveito de lea): godbolt.org/z/5jeQLQ . Com o BMI1, a versão de Eric ainda é melhor em x86-64, pelo menos em Intel onde blsihá um único uop, mas é 2 uops em AMD.
Peter Cordes
15

Não tenho certeza sobre rápido, mas pode fazer uma linha verificando que val^(val>>1)tem no máximo 2 bits ativados.

Isso só funciona com tipos não assinados: 0é necessário deslocar a no topo (deslocamento lógico), não um deslocamento aritmético à direita que desloca em uma cópia do bit de sinal.

#include <bitset>
bool has_compact_bits(unsigned val)
{
    return std::bitset<8*sizeof(val)>((val ^ (val>>1))).count() <= 2;
}

Para rejeitar 0(ou seja, aceitar apenas entradas que tenham exatamente 1 grupo de bits contíguo), AND lógico com valsendo diferente de zero. Outras respostas a esta questão aceitam 0como compactas.

bool has_compact_bits(unsigned val)
{
    return std::bitset<8*sizeof(val)>((val ^ (val>>1))).count() <= 2 and val;
}

C ++ expõe popcount via portabilidade std::bitset::count(), ou em C ++ 20 viastd::popcount . C ainda não tem uma maneira portátil que compila de forma confiável para um popcnt ou instrução semelhante em destinos onde um está disponível.

Yuri Feldman
fonte
2
Também o mais rápido, até agora.
Giovanni Cerretani
2
Acho que você precisa usar um tipo sem sinal para ter certeza de mudar em zeros, não cópias do bit de sinal. Considere 11011111. Aritmética deslocado para a direita, ele se torna 11101111, e o XOR é 00110000. Com o deslocamento lógico para a direita (deslocando em a 0no topo), você obtém 10110000e detecta corretamente os vários grupos de bits. Editando para consertar isso.
Peter Cordes
3
Isso é muito inteligente. Por mais que eu não __builtin_popcount()goste do estilo (IMO apenas uso , todo compilador tem um primitivo como esse hoje em dia), este é de longe o mais rápido (em uma cpu moderna). Na verdade, vou argumentar que essa apresentação é importante, porque em uma cpu que não tem POPCNT como uma única instrução, minha implementação pode superar isso. Portanto, se você for usar essa implementação, deve usar apenas o intrínseco. std::bitsettem uma interface horrível.
KevinZ
9

CPUs têm instruções dedicadas para isso, muito rápidas. No PC, eles são BSR / BSF (introduzido em 80386 em 1985), no ARM eles são CLZ / CTZ

Use um para encontrar o índice do conjunto de bits menos significativo, desloque o inteiro certo por esse valor. Use outro para encontrar um índice do bit de conjunto mais significativo, compare seu inteiro com (1u << (bsr + 1)) - 1.

Infelizmente, 35 anos não foram suficientes para atualizar a linguagem C ++ para corresponder ao hardware. Para usar essas instruções em C ++, você precisará de intrínsecos, eles não são portáteis e retornam resultados em formatos ligeiramente diferentes. Use o pré-processador, #ifdefetc, para detectar o compilador e, em seguida, use os intrínsecos apropriados. Em MSVC eles são _BitScanForward, _BitScanForward64, _BitScanReverse, _BitScanReverse64. No GCC e no clang, eles são __builtin_clze __builtin_ctz.

Soonts
fonte
2
@ e2-e4 Visual studio não oferece suporte a montagem em linha ao compilar para AMD64. É por isso que recomendo intrínsecos.
Soonts
5
Desde C ++ 20 existem std::countr_zeroe std::countl_zero. Caso você esteja usando Boost, ele possui wrappers portáteis chamados boost::multiprecision::lsbe boost::multiprecision::msb.
Giovanni Cerretani
8
Isso não responde a minha pergunta de forma alguma - eu me pergunto por que recebeu votos positivos
Walter
3
@Walter O que você quer dizer com “não responde”? Eu respondi exatamente o que você deve fazer, usar o pré-processador e depois o intrínseco.
Soonts
2
Aparentemente, o C ++ 20 finalmente está adicionando #include <bit> en.cppreference.com/w/cpp/header/bit com bit-scan, popcount e rotate. É patético que tenha demorado tanto para expor a varredura de bits de forma portátil, mas agora é melhor do que nunca. (Popcnt portátil está disponível através de std::bitset::count().) C ++ 20 ainda está faltando algumas coisas que Rust fornece ( doc.rust-lang.org/std/primitive.i32.html ), por exemplo, bit-reverse e endian que algumas CPUs fornecem eficientemente mas nem todos. Um portátil integrado para uma operação que qualquer CPU possui faz algum sentido, embora os usuários precisem saber o que é rápido.
Peter Cordes
7

A comparação com zeros em vez de uns salvará algumas operações:

bool has_compact_bits2(int val) {
    if (val == 0) return true;
    int h = __builtin_clz(val);
    // Clear bits to the left
    val = (unsigned)val << h;
    int l = __builtin_ctz(val);
    // Invert
    // >>l - Clear bits to the right
    return (~(unsigned)val)>>l == 0;
}

O seguinte resulta em uma instrução a menos que a acima gcc10 -O3em x86_64 e usa a extensão on sign:

bool has_compact_bits3(int val) {
    if (val == 0) return true;
    int h = __builtin_clz(val);
    val <<= h;
    int l = __builtin_ctz(val);
    return ~(val>>l) == 0;
}

Testado em godbolt .

KamilCuk
fonte
infelizmente, isso não é portátil. Sempre tenho medo de errar a precendência do operador com esses operadores de turno - você tem certeza ~val<<h>>h>>l == 0que faz o que pensa que faz?
Walter
4
Sim, tenho certeza, editei e adicionei colchetes de qualquer maneira. Och, então você está interessado em uma solução portátil? Porque eu olhei there exists a faster way?e assumi que vale tudo.
KamilCuk
5

Você pode reformular o requisito:

  • defina N o número de bits que são diferentes do anterior (iterando através dos bits)
  • se N = 2 e e o primeiro ou último bit for 0, então a resposta é sim
  • se N = 1, então a resposta é sim (porque todos os 1s estão de um lado)
  • se N = 0 então e qualquer bit é 0 então você não tem 1s, depende de você se considera a resposta ser sim ou não
  • qualquer outra coisa: a resposta é não

Passar por todos os bits pode ficar assim:

unsigned int count_bit_changes (uint32_t value) {
  unsigned int bit;
  unsigned int changes = 0;
  uint32_t last_bit = value & 1;
  for (bit = 1; bit < 32; bit++) {
    value = value >> 1;
    if (value & 1 != last_bit  {
      changes++;
      last_bit = value & 1;
    }
  }
  return changes;
}

Mas isso pode certamente ser otimizado (por exemplo, abortando o forloop quando valueatingido, o 0que significa que não há mais bits significativos com valor 1).

Brecht Sanders
fonte
3

Você pode fazer esta sequência de cálculos (assumindo valcomo uma entrada):

uint32_t x = val;
x |= x >>  1;
x |= x >>  2;
x |= x >>  4;
x |= x >>  8;
x |= x >> 16;

para obter um número com todos os zeros abaixo do mais significativo 1preenchido com uns.

Você também pode calcular y = val & -valpara retirar todos, exceto o 1 bit menos significativo em val(por exemplo, 7 & -7 == 1e 12 & -12 == 4).
Aviso: isso irá falhar paraval == INT_MIN , então você terá que lidar com este caso separadamente, mas isso é imediato.

Em seguida, mude para a direita yem uma posição, para ficar um pouco abaixo do LSB real de val, e faça a mesma rotina de x:

uint32_t y = (val & -val) >> 1;
y |= y >>  1;
y |= y >>  2;
y |= y >>  4;
y |= y >>  8;
y |= y >> 16;

Em seguida, x - you x & ~you x ^ yproduz a máscara de bits 'compacta' abrangendo todo o comprimento de val. Basta compará-lo para valver se ele valé 'compacto'.

CiaPan
fonte
2

Podemos usar as instruções internas do gcc para verificar se:

A contagem de bits definidos

int __builtin_popcount (unsigned int x)
Retorna o número de 1 bits em x.

é igual a (a - b):

a : Índice do bit mais alto definido (32 - CTZ) (32 porque 32 bits em um inteiro sem sinal).

int __builtin_clz (unsigned int x)
Retorna o número de bits 0 iniciais em x, começando na posição de bit mais significativa. Se x for 0, o resultado é indefinido.

b : Índice do bit mais baixo definido (CLZ):

int __builtin_clz (unsigned int x)
Retorna o número de bits 0 iniciais em x, começando na posição de bit mais significativa. Se x for 0, o resultado é indefinido.

Por exemplo, se n = 0b0001100110; obteremos 4 com popcount, mas a diferença de índice (a - b) retornará 6.

que também pode ser escrito como:

Não acho que seja mais elegante ou eficiente do que a resposta mais votada atualmente:

com a seguinte montagem:

mov     eax, edi
neg     eax
and     eax, edi
add     eax, edi
test    eax, edi
sete    al

mas provavelmente é mais fácil de entender.

Antonin GAVREL
fonte
1

Ok, aqui está uma versão que percorre bits

template<typename Integer>
inline constexpr bool has_compact_bits(Integer val) noexcept
{
    Integer test = 1;
    while(!(test & val) && test) test<<=1; // skip unset bits to find first set bit
    while( (test & val) && test) test<<=1; // skip set bits to find next unset bit
    while(!(test & val) && test) test<<=1; // skip unset bits to find an offending set bit
    return !test;
}

Os primeiros dois loops encontraram a primeira região compacta. O loop final verifica se há algum outro bit definido além dessa região.

Walter
fonte