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 m
e n
.
c++
c
bit-manipulation
Walter
fonte
fonte
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.h>=l
pela funcionalidade (implícita) dehighest_set_bit()
elowest_set_bit()
Respostas:
static _Bool IsCompact(unsigned x) { return (x & x + (x & -x)) == 0; }
Resumidamente:
x & -x
dá o bit mais baixo definido emx
(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~x
e o primeiro bit 0, mas depois pára. Portanto, os bits mais baixos de-x
até e incluindo o primeiro 1 são iguais aos bits mais baixos dex
, mas todos os bits mais altos são invertidos. (Exemplo:~10011100
dá01100011
, e adicionando 1 dá01100100
, então o mínimo100
é o mesmo, mas o alto10011
é invertido01100
.) Entãox & -x
nos dá o único bit que é 1 em ambos, que é o bit 1 mais baixo (00000100
). (Sex
for zero,x & -x
será zero.)Somando isso a
x
faz 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.fonte
x & -x
em uma únicablsi
instruçã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._Bool
é uma palavra-chave padrão, de acordo com C 2018 6.4.1 1.unsigned
. Se você deseja realizar o teste para um complemento de dois assinadoint
, a maneira mais fácil é simplesmente repassá-lo para a rotina nesta resposta, deixando que oint
seja convertido paraunsigned
. Isso dará o resultado desejado. Aplicar as operações mostradas a um assinadoint
diretamente pode ser problemático, devido a problemas de estouro / transporte. (Se você quiser testar o complemento ou sinal e magnitude de umaint
, isso é outro assunto, em grande parte apenas de interesse teórico atualmente.)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; }
fonte
-mtbm
, explorandoblsfill
/blcfill
instruções. Seria a versão mais curta proposta até agora. Infelizmente, quase nenhum processador suporta essa extensão de conjunto de instruções .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_ctz
porstd::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ão
boost::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 destd::mt19937
:__builtin_popcount
): 4.1 sEntã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 -mbmi
para obterpopcnt
eblsi
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.
fonte
return (x & x + (x & -x)) == 0;
.gcc -O3 -march=nehalem
(para disponibilizar popcnt), ou menos se BMI1blsi
estiver disponível parax & -x
: godbolt.org/z/zuyj_f . E as instruções são todas simples single-uop, exceto para apopcnt
versã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 oand val
do Yuri ou ele seria mais lento.mov
e não consegue tirar proveito delea
): godbolt.org/z/5jeQLQ . Com o BMI1, a versão de Eric ainda é melhor em x86-64, pelo menos em Intel ondeblsi
há um único uop, mas é 2 uops em AMD.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 comval
sendo diferente de zero. Outras respostas a esta questão aceitam0
como 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.fonte
11011111
. Aritmética deslocado para a direita, ele se torna11101111
, e o XOR é00110000
. Com o deslocamento lógico para a direita (deslocando em a0
no topo), você obtém10110000
e detecta corretamente os vários grupos de bits. Editando para consertar isso.__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::bitset
tem uma interface horrível.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,
#ifdef
etc, 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_clz
e__builtin_ctz
.fonte
std::countr_zero
estd::countl_zero
. Caso você esteja usando Boost, ele possui wrappers portáteis chamadosboost::multiprecision::lsb
eboost::multiprecision::msb
.#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 destd::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.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 -O3
em 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 .
fonte
~val<<h>>h>>l == 0
que faz o que pensa que faz?there exists a faster way?
e assumi que vale tudo.Você pode reformular o requisito:
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
for
loop quandovalue
atingido, o0
que significa que não há mais bits significativos com valor 1).fonte
Você pode fazer esta sequência de cálculos (assumindo
val
como 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
1
preenchido com uns.Você também pode calcular
y = val & -val
para retirar todos, exceto o 1 bit menos significativo emval
(por exemplo,7 & -7 == 1
e12 & -12 == 4
).Aviso: isso irá falhar para
val == INT_MIN
, então você terá que lidar com este caso separadamente, mas isso é imediato.Em seguida, mude para a direita
y
em uma posição, para ficar um pouco abaixo do LSB real deval
, e faça a mesma rotina dex
:uint32_t y = (val & -val) >> 1; y |= y >> 1; y |= y >> 2; y |= y >> 4; y |= y >> 8; y |= y >> 16;
Em seguida,
x - y
oux & ~y
oux ^ y
produz a máscara de bits 'compacta' abrangendo todo o comprimento deval
. Basta compará-lo paraval
ver se eleval
é 'compacto'.fonte
Podemos usar as instruções internas do gcc para verificar se:
A contagem de bits definidos
é igual a (a - b):
a : Índice do bit mais alto definido (32 - CTZ) (32 porque 32 bits em um inteiro sem sinal).
b : Índice do bit mais baixo definido (CLZ):
Por exemplo, se n = 0b0001100110; obteremos 4 com popcount, mas a diferença de índice (a - b) retornará 6.
bool has_contiguous_one_bits(unsigned n) { return (32 - __builtin_clz(n) - __builtin_ctz(n)) == __builtin_popcount(n); }
que também pode ser escrito como:
bool has_contiguous_one_bits(unsigned n) { return (__builtin_popcount(n) + __builtin_clz(n) + __builtin_ctz(n)) == 32; }
Não acho que seja mais elegante ou eficiente do que a resposta mais votada atualmente:
return (x & x + (x & -x)) == 0;
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.
fonte
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.
fonte