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 m
y n
.
fuente
0x0
es 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.h>=l
por la funcionalidad (implícita) dehighest_set_bit()
ylowest_set_bit()
Respuestas:
static _Bool IsCompact(unsigned x) { return (x & x + (x & -x)) == 0; }
Brevemente:
x & -x
da el bit más bajo establecido enx
(o cero six
es 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:
-x
es 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~x
y el primer bit 0, pero luego se detiene. Por lo tanto, los bits bajos de-x
hasta el primer 1 incluido son los mismos que los bits bajos dex
, pero todos los bits más altos se invierten. (Ejemplo:~10011100
da01100011
, y sumando 1 da01100100
, entonces los bajos100
son iguales, pero los altos10011
se cambian a01100
). Luegox & -x
nos da el único bit que es 1 en ambos, que es el 1 bit más bajo (00000100
). (Six
es cero,x & -x
es cero).Agregar esto
x
provoca 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.fuente
x & -x
en una solablsi
instrucció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._Bool
es una palabra clave estándar, según C 2018 6.4.1 1.unsigned
. Si desea realizar la prueba para un complemento a dos con signoint
, la forma más fácil es simplemente pasarlo a la rutina en esta respuesta, dejando queint
se convierta aunsigned
. Eso dará el resultado deseado. Aplicar el show de operaciones a un firmadoint
directamente puede ser problemático debido a problemas de desbordamiento / acarreo. (Si desea probar el complemento o el signo y la magnitud de unoint
, ese es otro asunto, en gran parte solo de interés teórico en estos días).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; }
fuente
-mtbm
, explotaciónblsfill
/blcfill
instrucciones. Sería la versión más corta propuesta hasta ahora. Desafortunadamente, casi ningún procesador admite esa extensión de conjunto de instrucciones .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_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; }
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 destd::mt19937
:__builtin_popcount
): 4.1 sEntonces, 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 -mbmi
llegarpopcnt
yblsi
la 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.
fuente
return (x & x + (x & -x)) == 0;
.gcc -O3 -march=nehalem
(para que popcnt esté disponible), o menos si BMI1blsi
está disponible parax & -x
: godbolt.org/z/zuyj_f . Y las instrucciones son todas simples de un solo uop, exceptopopcnt
la versión de Yuri que tiene latencia de 3 ciclos. (Pero supongo que estabas procesando el rendimiento). También supongo que debes haber eliminado eland val
de Yuri o sería más lento.mov
y no se aprovechalea
): godbolt.org/z/5jeQLQ . Con BMI1, la versión de Eric es aún mejor en x86-64, al menos en Intel dondeblsi
hay un solo uop, pero es 2 uops en AMD.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:
0
es 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 conval
un valor distinto de cero. Otras respuestas a esta pregunta se aceptan0
como 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.fuente
11011111
. Aritmética desplazada a la derecha, se convierte en11101111
, y el XOR es00110000
. Con un desplazamiento lógico a la derecha (desplazando a0
en la parte superior), obtiene10110000
y detecta correctamente los múltiples grupos de bits. Editando para arreglar eso.__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::bitset
tiene una interfaz horrible.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,
#ifdef
etc., 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_clz
y__builtin_ctz
.fuente
std::countr_zero
ystd::countl_zero
. En caso de que esté utilizando Boost, tiene contenedores portátiles llamadosboost::multiprecision::lsb
yboost::multiprecision::msb
.#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 destd::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.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 -O3
en 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 .
fuente
~val<<h>>h>>l == 0
hace lo que cree que hace?there exists a faster way?
y asumí que todo vale.Puede reformular el requisito:
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
for
bucle cuando sevalue
alcanza, lo0
que significa que no hay más bits significativos con valor 1).fuente
Puede hacer esta secuencia de cálculos (asumiendo
val
como 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
1
relleno con unos.También puede calcular
y = val & -val
para eliminar todo excepto el bit menos significativoval
(por ejemplo,7 & -7 == 1
y12 & -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
y
una posición, para llegar un poco por debajo del LSB real deval
, y haga la misma rutina que parax
:uint32_t y = (val & -val) >> 1; y |= y >> 1; y |= y >> 2; y |= y >> 4; y |= y >> 8; y |= y >> 16;
Luego,
x - y
orx & ~y
ox ^ y
produce la máscara de bits 'compacta' que abarca toda la longitud deval
. Simplemente compáreloval
para ver sival
es 'compacto'.fuente
Podemos hacer uso de las instrucciones integradas de gcc para verificar si:
El recuento de bits establecidos
es igual a (a - b):
a : Índice del bit establecido más alto (32 - CTZ) (32 porque 32 bits en un entero sin signo).
b : Índice del bit establecido más bajo (CLZ):
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.
fuente
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.
fuente