Esiste un modo elegante e veloce per verificare che 1 bit in un numero intero si trovi in ​​una regione contigua?


84

Devo verificare se le posizioni (da 0 a 31 per un numero intero a 32 bit) con valore di bit 1 formano una regione contigua. Per esempio:

00111111000000000000000000000000      is contiguous
00111111000000000000000011000000      is not contiguous

Voglio che questo test, cioè qualche funzione has_contiguous_one_bits(int), sia portatile.

Un modo ovvio è quello di scorrere le posizioni per trovare il primo bit impostato, quindi il primo bit non impostato e verificare la presenza di altri bit impostati.

Mi chiedo se esiste un modo più veloce? Se ci sono metodi veloci per trovare i bit impostati più alti e più bassi (ma da questa domanda sembra che non ce ne siano di portabili), allora una possibile implementazione è

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);
}

Solo per divertimento, ecco i primi 100 numeri interi con bit contigui:

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

sono (ovviamente) della forma (1<<m)*(1<<n-1)con non negativo me n.


4
@aafulei sì, 0x0è compatto. È più facile definire l'opposto (non compatto): se ci sono due bit impostati, tra di loro c'è almeno un bit non impostato.
Walter

1
@KamilCuk h>=ldalla funzionalità (implicita) di highest_set_bit()elowest_set_bit()
Walter


6
Quel collegamento OEIS dice che questi numeri hanno le loro cifre non crescenti quando sono in binario. Un altro modo per riferirsi a loro sarebbe dire che sono contigui (o forse collegati). Per questo matematico, "compatto" significa qualcosa di molto diverso.
Teepeemm

1
@Teepeemm Penso che uno dei motivi per cui questa domanda sia finita su domande hot network sia proprio a causa di questo uso improprio della parola compact, è certamente il motivo per cui ci ho cliccato sopra: non stavo pensando molto e mi chiedevo come potesse avere senso definire la compattezza quel modo. Ovviamente non ha senso.
Nessuno il

Risposte:


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

Brevemente:

x & -xfornisce il bit più basso impostato in x(o zero se xè zero).

x + (x & -x) converte la stringa più bassa di 1 consecutivi in ​​un singolo 1 (o va a capo a zero).

x & x + (x & -x) cancella quei 1 bit.

(x & x + (x & -x)) == 0 verifica se rimangono altri 1 bit.

Più a lungo:

-xè uguale ~x+1, usando il complemento a due, che assumiamo. Dopo che i bit sono stati capovolti ~x, l'aggiunta di 1 porta in modo che ribalti indietro i bit bassi 1 ~xe il primo bit 0, ma poi si ferma. Pertanto, i bit bassi -xfino al primo 1 compreso sono gli stessi dei bit bassi di x, ma tutti i bit superiori vengono invertiti. (Esempio: ~1001110001100011e aggiungendo 1 dà 01100100, quindi i bassi 100sono uguali, ma gli alti 10011vengono invertiti 01100.) Quindi x & -xci dà l'unico bit che è 1 in entrambi, che è l'1 bit più basso ( 00000100). (Se xè zero, x & -xè zero.)

L'aggiunta di questo a xcausa un riporto di tutti gli 1 consecutivi, cambiandoli in 0. Lascerà un 1 al successivo bit 0 più alto (o porterà fino all'estremità alta, lasciando un totale avvolto di zero) ( 10100000.)

Quando è associato a AND x, ci sono 0 nei punti in cui gli 1 sono stati modificati in 0 (e anche dove il riporto ha cambiato da 0 a 1). Quindi il risultato non è zero solo se c'è un altro 1 bit più in alto.


23
Almeno qualcuno conosce il libro Hacker's Delight. Si prega di consultare il capitolo 2-1 per la risposta. Ma questo è già stato risposto più volte qui su SO. Comunque: +1
Armin Montigny

33
Spero che se mai scriverai un codice del genere in produzione, includerai la spiegazione nei commenti;)
Polygnome

14
Questo trae vantaggio da x86 BMI1 da eseguire x & -xin una singola blsiistruzione, che è 1 uop su Intel, 2 uop su AMD Zen. godbolt.org/z/5zBx-A . Ma senza BMI1, la versione di @ KevinZ è ancora più efficiente.
Peter Cordes

3
@TommyAndersen: _Boolè una parola chiave standard, secondo C 2018 6.4.1 1.
Eric Postpischil

1
@ Walter: Hmm? Questo codice utilizza unsigned. Se vuoi eseguire il test per un complemento di due firmato int, il modo più semplice è passarlo semplicemente alla routine in questa risposta, lasciando che intvenga convertito in unsigned. Questo darà il risultato desiderato. L'applicazione diretta delle operazioni mostrate a un firmato intpuò essere problematica, a causa di problemi di overflow / carry. (Se vuoi testare un proprio complemento o segno e grandezza int, questa è un'altra questione, in gran parte solo di interesse teorico in questi giorni.)
Eric Postpischil

29

In realtà non è necessario utilizzare alcun elemento intrinseco.

Innanzitutto capovolgi tutti gli 0 prima del primo 1. Quindi verifica se il nuovo valore è un numero mersenne. In questo algoritmo, zero è mappato a vero.

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 ) );
}

Ovviamente, se vuoi usare gli intrinseci, ecco il metodo 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;
}

2
La prima versione si riduce a sole 4 istruzioni se compilata con -mtbm, sfruttando blsfill/ blcfillistruzioni. Sarebbe la versione più breve proposta finora. Sfortunatamente, quasi nessun processore supporta l'estensione del set di istruzioni .
Giovanni Cerretani

18

In realtà non è necessario contare gli zeri iniziali. Come suggerito da pmg nei commenti, sfruttando il fatto che i numeri che stai cercando sono quelli della sequenza OEIS A023758 , ovvero Numeri della forma 2 ^ i - 2 ^ j con i> = j , puoi semplicemente contare gli zeri finali ( cioè j - 1 ), alterna quei bit nel valore originale (equivalente ad aggiungere 2 ^ j - 1 ), e poi controlla se quel valore è nella forma 2 ^ i - 1 . Con le caratteristiche intrinseche di 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;
}

Questa versione è leggermente più veloce della tua e quella proposta da KamilCuk e quella di Yuri Feldman con solo popcount.

Se stai usando C ++ 20, potresti ottenere una funzione portabile sostituendola __builtin_ctzcon 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;
}

Il cast è brutto, ma ti avverte che è meglio lavorare con i tipi senza segno quando si manipolano i bit. Le alternative pre-C ++ 20 sono boost::multiprecision::lsb.

Modificare:

Il benchmark sul link barrato era limitato dal fatto che nessuna istruzione di conteggio pop era stata emessa per la versione di Yuri Feldman. Cercando di compilarli sul mio PC con -march=westmere, ho misurato il seguente tempo per 1 miliardo di iterazioni con sequenze identiche da std::mt19937:

  • la tua versione: 5.7 s
  • Seconda versione di KamilCuk: 4.7 s
  • la mia versione: 4.7 s
  • Prima versione di Eric Postpischil: 4.3 s
  • Versione di Yuri Feldman (usando esplicitamente __builtin_popcount): 4.1 s

Quindi, almeno sulla mia architettura, il più veloce sembra essere quello con popcount.

Modifica 2:

Ho aggiornato il mio benchmark con la nuova versione di Eric Postpischil. Come richiesto nei commenti, il codice del mio test può essere trovato qui . Ho aggiunto un ciclo no-op per stimare il tempo necessario al PRNG. Ho anche aggiunto le due versioni di KevinZ. Il codice è stato compilato su clang con -O3 -msse4 -mbmiper ottenere popcnte blsiistruzioni (grazie a Peter Cordes).

Risultati: Almeno sulla mia architettura, la versione di Eric Postpischil è esattamente veloce quanto quella di Yuri Feldman, e almeno due volte più veloce di qualsiasi altra versione proposta finora.


Ho rimosso un'operazione: return (x & x + (x & -x)) == 0;.
Eric Postpischil

3
Questo è il benchmark di una versione precedente della versione di @Eric, giusto? Con la versione corrente, Eric compila il minor numero di istruzioni con gcc -O3 -march=nehalem(per rendere disponibile popcnt), o meno se BMI1 blsiè disponibile per x & -x: godbolt.org/z/zuyj_f . E le istruzioni sono tutte semplici, ad eccezione popcntdella versione di Yuri che ha una latenza di 3 cicli. (Ma presumo che stavi mettendo in panchina il throughput.) Presumo anche che tu abbia rimosso il and valda Yuri o sarebbe più lento.
Peter Cordes

2
Inoltre, su quale hardware hai effettuato il benchmark? Collegare il tuo codice di benchmark completo su Godbolt o qualcosa del genere sarebbe una buona idea, in modo che i futuri lettori possano facilmente testare la loro implementazione C ++.
Peter Cordes

2
Dovresti anche testare la versione di @ KevinZ; compila ancora meno istruzioni senza BMI1 (almeno con clang; la versione non inline di gcc spreca una move non riesce a trarne vantaggio lea): godbolt.org/z/5jeQLQ . Con BMI1, la versione di Eric è ancora migliore su x86-64, almeno su Intel dove blsic'è un singolo uop, ma è 2 uop su AMD.
Peter Cordes

15

Non sono sicuro che sia veloce, ma puoi fare una riga verificando che val^(val>>1)abbia al massimo 2 bit.

Funziona solo con i tipi senza segno: è necessario lo spostamento in a 0in alto (spostamento logico), non uno spostamento aritmetico a destra che si sposta in una copia del bit del segno.

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

Per rifiutare 0(cioè accettare solo ingressi che hanno esattamente 1 gruppo di bit contiguo), AND logico con valore valdiverso da zero. Altre risposte su questa domanda accettano 0come compatte.

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

C ++ espone in modo portabile il conteggio pop tramite std::bitset::count(), o in C ++ 20 tramitestd::popcount . C ancora non ha un modo portabile che compili in modo affidabile in un popcnt o istruzioni simili sui target in cui è disponibile.


2
Anche il più veloce, finora.
Giovanni Cerretani

2
Penso che sia necessario utilizzare un tipo senza segno per assicurarsi di spostare gli zeri, non le copie del bit di segno. Considera 11011111. Aritmetica spostata a destra, diventa 11101111, e lo XOR lo è 00110000. Con lo spostamento logico a destra (spostando in una 0in alto), si ottengono 10110000e rilevano correttamente i più gruppi di bit. Modifica per risolverlo.
Peter Cordes

3
Questo è davvero intelligente. Per quanto non mi piaccia lo stile (IMO lo uso solo __builtin_popcount(), ogni compilatore ha una primitiva come quella al giorno d'oggi), questo è di gran lunga il più veloce (su una moderna cpu). In effetti, sosterrò che quella presentazione è davvero importante, perché su una CPU che non ha POPCNT come singola istruzione, la mia implementazione potrebbe battere questo. Pertanto, se intendi utilizzare questa implementazione, dovresti semplicemente usare l'intrinsic. std::bitsetha un'interfaccia orribile.
KevinZ

9

Le CPU hanno istruzioni dedicate per questo, molto veloci. Su PC sono BSR / BSF (introdotti nell'80386 nel 1985), su ARM sono CLZ / CTZ

Usane uno per trovare l'indice del bit impostato meno significativo, sposta l'intero a destra di quella quantità. Usane un altro per trovare un indice del bit impostato più significativo, confronta il tuo numero intero con (1u << (bsr + 1)) - 1.

Sfortunatamente, 35 anni non sono stati sufficienti per aggiornare il linguaggio C ++ in modo che corrispondesse all'hardware. Per utilizzare queste istruzioni da C ++ avrai bisogno di elementi intrinseci, questi non sono portabili e restituiscono risultati in formati leggermente diversi. Utilizzare il preprocessore, #ifdefecc., Per rilevare il compilatore e quindi utilizzare gli intrinseci appropriati. In MSVC sono _BitScanForward, _BitScanForward64, _BitScanReverse, _BitScanReverse64. In GCC e clang sono __builtin_clze __builtin_ctz.


2
@ e2-e4 Visual studio non supporta l'assembly inline durante la compilazione per AMD64. Ecco perché raccomando gli intrinseci.
Soonts

5
Dal momento che C ++ 20 ci sono std::countr_zeroe std::countl_zero. Nel caso in cui utilizzi Boost, ha wrapper portatili chiamati boost::multiprecision::lsbe boost::multiprecision::msb.
Giovanni Cerretani

8
Questo non risponde affatto alla mia domanda - mi chiedo perché abbia ricevuto voti positivi
Walter

3
@ Walter Cosa intendi con "non risponde"? Ho risposto esattamente a cosa dovresti fare, usa il preprocessore e quindi gli intrinseci.
Soonts

2
Apparentemente C ++ 20 sta finalmente aggiungendo #include <bit> en.cppreference.com/w/cpp/header/bit con bit-scan, popcount e rotate. È patetico che ci sia voluto così tanto tempo per esporre in modo portabile il bit-scan, ma ora è meglio che mai. (Il popcnt portatile è stato disponibile tramite std::bitset::count().) C ++ 20 manca ancora di alcune cose che Rust fornisce ( doc.rust-lang.org/std/primitive.i32.html ), ad esempio bit-reverse e endian che alcune CPU forniscono in modo efficiente ma non tutto. Un builtin portatile per un'operazione che qualsiasi CPU ha ha un senso, anche se gli utenti devono sapere cosa è veloce.
Peter Cordes

7

Il confronto con zeri anziché uno salverà alcune operazioni:

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;
}

I seguenti risultati in un'istruzione in meno rispetto a quanto sopra gcc10 -O3su x86_64 e utilizza l'estensione del segno:

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;
}

Testato su Godbolt .


sfortunatamente, questo non è portatile. Ho sempre paura di fraintendere la precedenza dell'operatore con quegli operatori di turno - sei sicuro che ~val<<h>>h>>l == 0faccia quello che pensi che faccia?
Walter

4
Sì, sono sicuro, modificato e aggiunto comunque le parentesi graffe. Och, quindi sei interessato a una soluzione portatile? Perché ho guardato there exists a faster way?e ho pensato che tutto andasse bene.
KamilCuk

5

Puoi riformulare il requisito:

  • impostare N il numero di bit diversi dal precedente (iterando tra i bit)
  • se N = 2 e e il primo o l'ultimo bit è 0, la risposta è sì
  • se N = 1 allora la risposta è sì (perché tutti gli 1 sono su un lato)
  • se N = 0 allora e ogni bit è 0 allora non hai 1, dipende da te se consideri la risposta sì o no
  • altro: la risposta è no

L'esame di tutti i bit potrebbe essere simile a questo:

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;
}

Ma questo può sicuramente essere ottimizzato (ad es. Interrompendo il forciclo una volta valueraggiunto, il 0che significa che non sono più presenti bit significativi con valore 1).


3

Puoi eseguire questa sequenza di calcoli (assumendo valcome input):

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

per ottenere un numero con tutti zeri sotto il più significativo 1riempito con uno.

Puoi anche calcolare y = val & -valdi rimuovere tutto tranne il bit meno significativo in val(ad esempio, 7 & -7 == 1e 12 & -12 == 4).
Attenzione: questo fallirà per val == INT_MIN, quindi dovrai gestire questo caso separatamente, ma questo è immediato.

Quindi sposta a destra ydi una posizione, per ottenere un po 'al di sotto dell'LSB effettivo di val, e fai la stessa routine di x:

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

Quindi x - yo x & ~yo x ^ yproduce la maschera di bit "compatta" che copre l'intera lunghezza di val. Basta confrontarlo con valper vedere se valè "compatto".


2

Possiamo utilizzare le istruzioni integrate di gcc per verificare se:

Il conteggio dei bit impostati

int __builtin_popcount (unsigned int x)
Restituisce il numero di 1 bit in x.

è uguale a (a - b):

a : Indice del bit impostato più alto (32 - CTZ) (32 perché 32 bit in un numero intero senza segno).

int __builtin_clz (unsigned int x)
Restituisce il numero di bit 0 iniziali in x, a partire dalla posizione del bit più significativo. Se x è 0, il risultato è indefinito.

b : Indice del bit impostato più basso (CLZ):

int __builtin_clz (unsigned int x)
Restituisce il numero di bit 0 iniziali in x, a partire dalla posizione del bit più significativo. Se x è 0, il risultato è indefinito.

Ad esempio, se n = 0b0001100110; otterremo 4 con popcount ma la differenza di indice (a - b) restituirà 6.

che può anche essere scritto come:

Non penso che sia più elegante o efficiente dell'attuale risposta più votata:

con il seguente montaggio:

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

ma probabilmente è più facile da capire.


1

Ok, ecco una versione che gira su bit

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;
}

I primi due loop hanno trovato la prima regione compatta. Il ciclo finale controlla se c'è qualche altro bit impostato oltre quella regione.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.