¿Existe una manera elegante y rápida de probar que los 1 bits de un entero estén en una región contigua?

85

Necesito probar si las posiciones (de 0 a 31 para un entero de 32 bits) con el valor de bit 1 forman una región contigua. Por ejemplo:

00111111000000000000000000000000      is contiguous
00111111000000000000000011000000      is not contiguous

Quiero que esta prueba, es decir, alguna función has_contiguous_one_bits(int), sea portátil.

Una forma obvia es recorrer las posiciones para encontrar el primer bit establecido, luego el primer bit no establecido y verificar si hay más bits establecidos.

Me pregunto si existe una forma más rápida. Si existen métodos rápidos para encontrar los bits de conjunto más alto y más bajo (pero a partir de esta pregunta parece que no hay ninguno portátil), entonces una posible implementación es

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 por diversión, aquí están los primeros 100 enteros con bits contiguos:

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

son (por supuesto) de la forma (1<<m)*(1<<n-1)con no negativos my n.

Walter
fuente
4
@aafulei sí, 0x0es compacto. Es más fácil definir lo contrario (no compacto): si hay dos bits establecidos, hay al menos un bit no establecido entre ellos.
Walter
1
@KamilCuk h>=lpor la funcionalidad (implícita) de highest_set_bit()ylowest_set_bit()
Walter
6
OEIS A023758
pmg
6
Ese enlace OEIS dice que estos números no tienen dígitos crecientes cuando están en binario. Otra forma de referirse a ellos sería decir que son contiguos (o tal vez conectados). Para este matemático, "compacto" significa algo muy diferente.
Teepeemm
1
@Teepeemm Creo que una de las razones por las que esta pregunta terminó en preguntas candentes de la red es exactamente por este mal uso de la palabra compacta, ciertamente es por eso que hice clic en ella: no estaba pensando mucho y me preguntaba cómo podría tener sentido definir compacidad de esa manera. Evidentemente no tiene sentido.
Nadie

Respuestas:

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

Brevemente:

x & -xda el bit más bajo establecido en x(o cero si xes cero).

x + (x & -x) convierte la cadena más baja de unos consecutivos en un solo 1 (o se ajusta a cero).

x & x + (x & -x) borra esos 1 bits.

(x & x + (x & -x)) == 0 comprueba si quedan otros 1 bits.

Más:

-xes igual ~x+1, usando el complemento a dos, que asumimos. Después de que los bits se invierten ~x, agregando 1 acarreo para que invierta los bits bajos 1 ~xy el primer bit 0, pero luego se detiene. Por lo tanto, los bits bajos de -xhasta el primer 1 incluido son los mismos que los bits bajos de x, pero todos los bits más altos se invierten. (Ejemplo: ~10011100da 01100011, y sumando 1 da 01100100, entonces los bajos 100son iguales, pero los altos 10011se cambian a 01100). Luego x & -xnos da el único bit que es 1 en ambos, que es el 1 bit más bajo ( 00000100). (Si xes cero, x & -xes cero).

Agregar esto xprovoca un arrastre de todos los 1 consecutivos, cambiándolos a 0. Dejará un 1 en el siguiente bit 0 más alto (o continuará hasta el extremo superior, dejando un total envuelto de cero) ( 10100000.)

Cuando se usa el AND x, hay 0 en los lugares donde los 1 se cambiaron a 0 (y también donde el acarreo cambió de 0 a 1). Entonces, el resultado no es cero solo si hay otro 1 bit más arriba.

Eric Postpischil
fuente
23
Al menos alguien conoce el libro Hacker's Delight. Consulte el capítulo 2-1 para obtener la respuesta. Pero esto ya ha sido respondido varias veces aquí en SO. De todos modos: +1
Armin Montigny
33
Espero que si alguna vez escribe dicho código en producción, incluya la explicación en los comentarios;)
Polygnome
14
Esto se beneficia muy bien de x86 BMI1 para hacerlo x & -xen una sola blsiinstrucción, que es 1 uop en Intel, 2 uop en AMD Zen. godbolt.org/z/5zBx-A . Pero sin BMI1, la versión de @ KevinZ es aún más eficiente.
Peter Cordes
3
@TommyAndersen: _Booles una palabra clave estándar, según C 2018 6.4.1 1.
Eric Postpischil
1
@Walter: ¿Hmm? Este código utiliza unsigned. Si desea realizar la prueba para un complemento a dos con signo int, la forma más fácil es simplemente pasarlo a la rutina en esta respuesta, dejando que intse convierta a unsigned. Eso dará el resultado deseado. Aplicar el show de operaciones a un firmado intdirectamente puede ser problemático debido a problemas de desbordamiento / acarreo. (Si desea probar el complemento o el signo y la magnitud de uno int, ese es otro asunto, en gran parte solo de interés teórico en estos días).
Eric Postpischil
29

En realidad, no es necesario utilizar ningún intrínseco.

Primero voltee todos los ceros antes del primer 1. Luego pruebe si el nuevo valor es un número de mersenne. En este algoritmo, cero se asigna a verdadero.

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

Por supuesto, si desea utilizar intrínsecos, aquí está el 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
fuente
2
La primera versión se reduce a solo 4 instrucciones si se compila con -mtbm, explotación blsfill/ blcfillinstrucciones. Sería la versión más corta propuesta hasta ahora. Desafortunadamente, casi ningún procesador admite esa extensión de conjunto de instrucciones .
Giovanni Cerretani
19

En realidad, no es necesario contar los ceros iniciales. Como sugiere pmg en los comentarios, aprovechando el hecho de que los números que está buscando son los de la secuencia OEIS A023758 , es decir, números de la forma 2 ^ i - 2 ^ j con i> = j , puede simplemente contar ceros finales ( es decir, j - 1 ), cambie esos bits en el valor original (equivalente a sumar 2 ^ j - 1 ), y luego verifique si ese valor es de la forma 2 ^ i - 1 . Con 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 versión es un poco más rápida que la tuya y la propuesta por KamilCuk y la de Yuri Feldman solo con popcount.

Si está utilizando C ++ 20, puede obtener una función portátil reemplazándola __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;
}

El elenco es feo, pero le advierte que es mejor trabajar con tipos sin firmar al manipular bits. Las alternativas anteriores a C ++ 20 son boost::multiprecision::lsb.

Editar:

El punto de referencia en el enlace tachado estaba limitado por el hecho de que no se había emitido ninguna instrucción popcount para la versión de Yuri Feldman. Al tratar de compilarlos en mi PC con -march=westmere, he medido el siguiente tiempo para mil millones de iteraciones con secuencias idénticas de std::mt19937:

  • tu versión: 5.7 s
  • Segunda versión de KamilCuk: 4.7 s
  • mi versión: 4.7 s
  • Primera versión de Eric Postpischil: 4.3 s
  • Versión de Yuri Feldman (usando explícitamente __builtin_popcount): 4.1 s

Entonces, al menos en mi arquitectura, el más rápido parece ser el que tiene popcount.

Edición 2:

He actualizado mi punto de referencia con la nueva versión de Eric Postpischil. Como se solicitó en los comentarios, el código de mi prueba se puede encontrar aquí . Agregué un bucle sin operación para estimar el tiempo que necesita el PRNG. También agregué las dos versiones de KevinZ. Código ha sido compilado en el sonido metálico con -O3 -msse4 -mbmillegar popcnty blsila instrucción (gracias a Peter Cordes).

Resultados: Al menos en mi arquitectura, la versión de Eric Postpischil es exactamente tan rápida como la de Yuri Feldman, y al menos dos veces más rápida que cualquier otra versión propuesta hasta ahora.

Giovanni Cerretani
fuente
Quité una operación: return (x & x + (x & -x)) == 0;.
Eric Postpischil
3
Esto es una evaluación comparativa de una versión anterior de la versión de @ Eric, ¿verdad? Con la versión actual, Eric's se compila con unas pocas instrucciones con gcc -O3 -march=nehalem(para que popcnt esté disponible), o menos si BMI1 blsiestá disponible para x & -x: godbolt.org/z/zuyj_f . Y las instrucciones son todas simples de un solo uop, excepto popcntla versión de Yuri que tiene latencia de 3 ciclos. (Pero supongo que estabas procesando el rendimiento). También supongo que debes haber eliminado el and valde Yuri o sería más lento.
Peter Cordes
2
Además, ¿en qué hardware comparó? Vincular su código de referencia completo en Godbolt o algo así sería una buena idea, para que los futuros lectores puedan probar fácilmente su implementación de C ++.
Peter Cordes
2
También debería probar la versión de @ KevinZ; se compila incluso con menos instrucciones sin BMI1 (al menos con clang; la versión no alineada de gcc desperdicia un movy no se aprovecha lea): godbolt.org/z/5jeQLQ . Con BMI1, la versión de Eric es aún mejor en x86-64, al menos en Intel donde blsihay un solo uop, pero es 2 uops en AMD.
Peter Cordes
15

No estoy seguro de que sea rápido, pero puede hacer una sola línea verificando que val^(val>>1)tenga como máximo 2 bits.

Esto solo funciona con tipos sin firmar: 0es necesario un cambio en la parte superior (cambio lógico), no un cambio aritmético a la derecha que cambia en una copia del bit de signo.

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

Rechazar 0(es decir, aceptar únicamente entradas que tengan exactamente 1 grupo de bits contiguo), Y lógico con valun valor distinto de cero. Otras respuestas a esta pregunta se aceptan 0como compactas.

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

C ++ expone de forma portátil popcount a través de std::bitset::count(), o en C ++ 20 a través destd::popcount . C todavía no tiene una forma portátil que compile de manera confiable a una instrucción popcnt o similar en los objetivos donde hay una disponible.

Yuri Feldman
fuente
2
También el más rápido, hasta ahora.
Giovanni Cerretani
2
Creo que debe usar un tipo sin firmar para asegurarse de cambiar a ceros, no copias del bit de signo. Considere 11011111. Aritmética desplazada a la derecha, se convierte en 11101111, y el XOR es 00110000. Con un desplazamiento lógico a la derecha (desplazando a 0en la parte superior), obtiene 10110000y detecta correctamente los múltiples grupos de bits. Editando para arreglar eso.
Peter Cordes
3
Esto es realmente inteligente. Por mucho que no me guste el estilo (en mi opinión, solo uso __builtin_popcount(), cada compilador tiene una primitiva como esa hoy en día), este es, con mucho, el más rápido (en una CPU moderna). De hecho, voy a argumentar que esa presentación es muy importante, porque en una CPU que no tiene POPCNT como una sola instrucción, mi implementación podría superar esto. Por lo tanto, si va a usar esta implementación, solo debe usar el intrínseco. std::bitsettiene una interfaz horrible.
KevinZ
9

Las CPU tienen instrucciones dedicadas para eso, muy rápido. En PC son BSR / BSF (introducido en 80386 en 1985), en ARM son CLZ / CTZ

Use uno para encontrar el índice del bit establecido menos significativo, cambie el entero a la derecha en esa cantidad. Utilice otro para encontrar un índice del conjunto de bits más significativo, compare su entero con (1u << (bsr + 1)) - 1.

Desafortunadamente, 35 años no fueron suficientes para actualizar el lenguaje C ++ para que coincida con el hardware. Para usar estas instrucciones de C ++, necesitará elementos intrínsecos, estos no son portátiles y devuelven resultados en formatos ligeramente diferentes. Utilice un preprocesador, #ifdefetc., para detectar el compilador y luego utilice los intrínsecos adecuados. En MSVC son _BitScanForward, _BitScanForward64, _BitScanReverse, _BitScanReverse64. En GCC y clang están __builtin_clzy __builtin_ctz.

Pronto
fuente
2
@ e2-e4 Visual Studio no admite el ensamblaje en línea al compilar para AMD64. Por eso recomiendo los intrínsecos.
Soonts
5
Desde C ++ 20 existen std::countr_zeroy std::countl_zero. En caso de que esté utilizando Boost, tiene contenedores portátiles llamados boost::multiprecision::lsby boost::multiprecision::msb.
Giovanni Cerretani
8
Esto no responde a mi pregunta en absoluto. Me pregunto por qué obtuvo votos a favor
Walter
3
@Walter ¿Qué quieres decir con "no responde"? He respondido precisamente lo que debe hacer, usar preprocesador y luego intrínsecos.
Soonts
2
Aparentemente, C ++ 20 finalmente está agregando #include <bit> en.cppreference.com/w/cpp/header/bit con bit-scan, popcount y rotate. Es patético que haya tardado tanto en exponer de forma portátil el escaneo de bits, pero ahora es mejor que nunca. (Portable popcnt ha estado disponible a través de std::bitset::count()). A C ++ 20 todavía le faltan algunas cosas que proporciona Rust ( doc.rust-lang.org/std/primitive.i32.html ), por ejemplo, bit-reverse y endian, que algunas CPU proporcionan de manera eficiente pero no todos. Un dispositivo portátil incorporado para una operación que tiene cualquier CPU tiene algún sentido, aunque los usuarios necesitan saber qué es rápido.
Peter Cordes
7

La comparación con ceros en lugar de unos salvará algunas operaciones:

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

Lo siguiente da como resultado una instrucción menos que la anterior gcc10 -O3en x86_64 y utiliza la extensión de signo:

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

Probado en godbolt .

KamilCuk
fuente
desafortunadamente, esto no es portátil. Siempre me temo que me equivoco en la precendencia del operador con esos operadores de turno. ¿Está seguro de que ~val<<h>>h>>l == 0hace lo que cree que hace?
Walter
4
Sí, estoy seguro, he editado y añadido llaves de todos modos. Och, ¿estás interesado en una solución portátil? Porque miré there exists a faster way?y asumí que todo vale.
KamilCuk
5

Puede reformular el requisito:

  • establecer N el número de bits que son diferentes al anterior (iterando a través de los bits)
  • si N = 2 y el primer o último bit es 0, la respuesta es sí
  • si N = 1 entonces la respuesta es sí (porque todos los 1 están en un lado)
  • si N = 0 entonces y cualquier bit es 0 entonces no tiene 1, depende de usted si considera que la respuesta es sí o no
  • cualquier otra cosa: la respuesta es no

Pasar por todos los bits podría verse así:

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

Pero esto seguramente se puede optimizar (por ejemplo, abortando el forbucle cuando se valuealcanza, lo 0que significa que no hay más bits significativos con valor 1).

Lijadoras de Brecht
fuente
3

Puede hacer esta secuencia de cálculos (asumiendo valcomo entrada):

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

para obtener un número con todos los ceros debajo del más significativo 1relleno con unos.

También puede calcular y = val & -valpara eliminar todo excepto el bit menos significativo val(por ejemplo, 7 & -7 == 1y 12 & -12 == 4).
Advertencia: esto fallará val == INT_MIN, por lo que tendrá que manejar este caso por separado, pero esto es inmediato.

Luego, cambie a la derecha yuna posición, para llegar un poco por debajo del LSB real de val, y haga la misma rutina que para x:

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

Luego, x - yor x & ~yo x ^ yproduce la máscara de bits 'compacta' que abarca toda la longitud de val. Simplemente compárelo valpara ver si vales 'compacto'.

CiaPan
fuente
2

Podemos hacer uso de las instrucciones integradas de gcc para verificar si:

El recuento de bits establecidos

int __builtin_popcount (unsigned int x)
Devuelve el número de 1 bits en x.

es igual a (a - b):

a : Índice del bit establecido más alto (32 - CTZ) (32 porque 32 bits en un entero sin signo).

int __builtin_clz (unsigned int x)
Devuelve el número de 0 bits iniciales en x, comenzando en la posición de bit más significativa. Si x es 0, el resultado no está definido.

b : Índice del bit establecido más bajo (CLZ):

int __builtin_clz (unsigned int x)
Devuelve el número de 0 bits iniciales en x, comenzando en la posición de bit más significativa. Si x es 0, el resultado no está definido.

Por ejemplo, si n = 0b0001100110; obtendremos 4 con popcount pero la diferencia de índice (a - b) devolverá 6.

bool has_contiguous_one_bits(unsigned n) {
    return (32 - __builtin_clz(n) - __builtin_ctz(n)) == __builtin_popcount(n);
}

que también se puede escribir como:

bool has_contiguous_one_bits(unsigned n) {
    return (__builtin_popcount(n) + __builtin_clz(n) + __builtin_ctz(n)) == 32;
}

No creo que sea más elegante o eficiente que la respuesta actual más votada:

return (x & x + (x & -x)) == 0;

con el siguiente montaje:

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

pero probablemente sea más fácil de entender.

Antonin GAVREL
fuente
1

De acuerdo, aquí hay una versión que recorre 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;
}

Los dos primeros bucles encontraron la primera región compacta. El ciclo final verifica si hay algún otro bit establecido más allá de esa región.

Walter
fuente