Estoy buscando una forma eficiente de determinar la posición del bit menos significativo que se establece en un entero, por ejemplo, para 0x0FF0 sería 4.
Una implementación trivial es esta:
unsigned GetLowestBitPos(unsigned value)
{
assert(value != 0); // handled separately
unsigned pos = 0;
while (!(value & 1))
{
value >>= 1;
++pos;
}
return pos;
}
¿Alguna idea de cómo sacarle algunos ciclos?
(Nota: esta pregunta es para personas que disfrutan de tales cosas, no para que me digan que la xzoptimización es mala).
[editar] ¡ Gracias a todos por las ideas! También he aprendido algunas otras cosas. ¡Frio!
c++
c
optimization
bit-manipulation
Peterchen
fuente
fuente
Respuestas:
Bit Twiddling Hacks ofrece una excelente colección de, er, pequeños trucos , con una discusión sobre rendimiento / optimización adjunta. Mi solución favorita para su problema (de ese sitio) es «multiplicar y buscar»:
Referencias útiles:
fuente
__builtin_ffsl
offsl
?¿Por qué no utilizar los ffs integrados ? (Tomé una página de manual de Linux, pero está más disponible que eso).
fuente
Hay una instrucción de ensamblaje x86 (
bsf
) que lo hará. :)¿Más optimizado?
Nota al margen:
La optimización en este nivel depende inherentemente de la arquitectura. Los procesadores actuales son demasiado complejos (en términos de predicción de rama, fallos de caché, canalización) que es tan difícil predecir qué código se ejecuta más rápido en qué arquitectura. Disminuir las operaciones de 32 a 9 o cosas por el estilo podría incluso disminuir el rendimiento en algunas arquitecturas. El código optimizado en una sola arquitectura puede resultar en un código peor en la otra. Creo que optimizaría esto para una CPU específica o lo dejaría como está y dejaría que el compilador elija lo que cree que es mejor.
fuente
La mayoría de las arquitecturas modernas tendrán alguna instrucción para encontrar la posición del bit establecido más bajo, o el bit establecido más alto, o contar el número de ceros iniciales, etc.
Si tiene alguna instrucción de esta clase, puede emular a las demás de forma económica.
Tómese un momento para trabajarlo en papel y darse cuenta de que
x & (x-1)
borrará el bit establecido más bajo en x, y( x & ~(x-1) )
devolverá solo el bit establecido más bajo, independientemente de la arquitectura, la longitud de la palabra, etc. Sabiendo esto, es trivial usar hardware de conteo líder -zeroes / mayor-conjunto-bit para encontrar el bit de conjunto más bajo si no hay una instrucción explícita para hacerlo.Si no hay soporte de hardware relevante en absoluto, la implementación de multiplicar y buscar de los ceros delanteros del recuento que se proporciona aquí iniciales o uno de los de la página Bit Twiddling Hacks se puede convertir trivialmente para dar el bit más bajo usando las identidades anteriores y tiene la ventaja de no tener ramificaciones.
fuente
Vaya, muchas soluciones y ni un punto de referencia a la vista. Ustedes deberían estar avergonzados de ustedes mismos ;-)
Mi máquina es una Intel i530 (2,9 GHz), con Windows 7 de 64 bits. Compilé con una versión de 32 bits de MinGW.
Mi código:
fuente
BSF
Tiene una dependencia falsa en su salida (ya que el comportamiento real cuando input = 0 es dejar la salida sin cambios). gcc desafortunadamente convierte esto en una dependencia de bucle al no borrar el registro entre iteraciones de bucle. Por lo tanto, el bucle debe ejecutarse en uno de cada 5 ciclos, cuello de botella en BSF (3) + CMOV (2) latencia.ffs()
debería haber tenido un rendimiento de uno por reloj (3 uops, 1 para BSF y 2 para CMOV, y pueden ejecutarse en diferentes puertos). Con la misma sobrecarga de bucle, son 7 uops ALU que pueden ejecutarse (en su CPU) a 3 por reloj. ¡Domina los gastos generales! Fuente: agner.org/optimizebsf ecx, [ebx+edx*4]
no se trataraecx
como una entrada que tenía que esperar. (ECX fue escrito por última vez por CMOV del iteraton anterior). Pero la CPU se comporta de esa manera, para implementar el comportamiento "dejar dest sin modificar si la fuente es cero" (por lo que no es realmente un depósito falso como lo es para TZCNT; se requiere una dependencia de datos porque no hay bifurcación + ejecución especulativa en la suposición que la entrada es distinta de cero). Podríamos superarlo agregando unxor ecx,ecx
antes delbsf
, para romper la dependencia de ECX.La solución más rápida (no intrínseca / no ensambladora) para esto es encontrar el byte más bajo y luego usar ese byte en una tabla de búsqueda de 256 entradas. Esto le da un rendimiento en el peor de los casos de cuatro instrucciones condicionales y en el mejor de los casos de 1. No solo es la menor cantidad de instrucciones, sino la menor cantidad de ramificaciones, lo cual es muy importante en el hardware moderno.
Su tabla (256 entradas de 8 bits) debe contener el índice del LSB para cada número en el rango 0-255. Verifica cada byte de su valor y encuentra el byte más bajo distinto de cero, luego usa este valor para buscar el índice real.
Esto requiere 256 bytes de memoria, pero si la velocidad de esta función es tan importante, entonces vale la pena usar 256 bytes.
P.ej
fuente
Dios mío tiene esto en espiral.
Lo que falta en la mayoría de estos ejemplos es un poco de comprensión sobre cómo funciona todo el hardware.
Siempre que tenga una rama, la CPU tiene que adivinar qué rama se tomará. La tubería de instrucciones se carga con las instrucciones que conducen por la ruta adivinada. Si la CPU ha adivinado mal, la tubería de instrucciones se vacía y se debe cargar la otra rama.
Considere el simple bucle while en la parte superior. La suposición será mantenerse dentro del ciclo. Estará mal al menos una vez cuando salga del bucle. Esto descargará la tubería de instrucciones. Este comportamiento es un poco mejor que adivinar que abandonará el bucle, en cuyo caso eliminaría la tubería de instrucciones en cada iteración.
La cantidad de ciclos de CPU que se pierden varía mucho de un tipo de procesador a otro. Pero puede esperar entre 20 y 150 ciclos de CPU perdidos.
El siguiente grupo peor es donde cree que va a ahorrar algunas iteraciones dividiendo el valor en partes más pequeñas y agregando varias ramas más. Cada una de estas ramas agrega una oportunidad adicional para descargar la tubería de instrucción y cuesta otros 20 a 150 ciclos de reloj.
Consideremos lo que sucede cuando busca un valor en una tabla. Es probable que el valor no esté actualmente en la caché, al menos no la primera vez que se llama a su función. Esto significa que la CPU se detiene mientras el valor se carga desde la caché. Nuevamente, esto varía de una máquina a otra. Los nuevos chips de Intel utilizan esto como una oportunidad para intercambiar subprocesos mientras el subproceso actual espera a que se complete la carga de la caché. Esto podría ser fácilmente más costoso que una descarga de tubería de instrucciones, sin embargo, si realiza esta operación varias veces, es probable que solo ocurra una vez.
Claramente, la solución de tiempo constante más rápida es aquella que involucra matemáticas deterministas. Una solución pura y elegante.
Mis disculpas si esto ya estaba cubierto.
Todos los compiladores que utilizo, excepto XCODE AFAIK, tienen elementos intrínsecos de compilador tanto para la exploración de bits directa como para la exploración de bits inversa. Estos se compilarán en una sola instrucción de ensamblaje en la mayoría de hardware sin Cache Miss, sin Branch Miss-Prediction y Ningún otro programador generó obstáculos.
Para los compiladores de Microsoft, use _BitScanForward & _BitScanReverse.
Para GCC, use __builtin_ffs, __builtin_clz, __builtin_ctz.
Además, absténgase de publicar una respuesta y engañar a los recién llegados si no tiene el conocimiento suficiente sobre el tema que se está discutiendo.
Lo siento, olvidé totalmente proporcionar una solución. Este es el código que uso en el iPad que no tiene instrucciones de nivel de ensamblaje para la tarea:
Lo que hay que entender aquí es que no es la comparación lo que es caro, sino la rama que se produce después de la comparación. En este caso, la comparación se fuerza a un valor de 0 o 1 con .. == 0, y el resultado se usa para combinar las matemáticas que se habrían producido en cualquier lado de la rama.
Editar:
El código anterior está totalmente roto. Este código funciona y aún no tiene ramificaciones (si está optimizado):
Esto devuelve -1 si se le da 0. Si no le importa 0 o está feliz de obtener 31 por 0, elimine el cálculo i0, ahorrando una gran cantidad de tiempo.
fuente
-O3
godbolt.org/z/gcsUHdInspirado por esta publicación similar que implica buscar un bit, ofrezco lo siguiente:
Pros:
Contras:
Actualización: como se señaló en los comentarios, una unión es una implementación más limpia (para C, al menos) y se vería así:
Esto supone entradas de 32 bits con almacenamiento little-endian para todo (piense en procesadores x86).
fuente
int
esint32_t
, y que firmó desplazamiento a la derecha es un desplazamiento aritmético (en C ++ es de aplicación definidos)Se puede hacer con el peor de los casos de menos de 32 operaciones:
Principio: comprobación de 2 o más bits es tan eficaz como la comprobación de 1 bit.
Entonces, por ejemplo, no hay nada que le impida verificar en qué agrupación está primero y luego verificar cada bit de menor a mayor en ese grupo.
Entonces ...
si verifica 2 bits a la vez, tiene en el peor de los casos (Nbits / 2) + 1 verificaciones en total.
si marca 3 bits a la vez, tiene en el peor de los casos (Nbits / 3) + 2 comprobaciones en total.
...
Lo óptimo sería verificar en grupos de 4. Lo que en el peor de los casos requeriría 11 operaciones en lugar de 32.
El mejor caso va desde la 1 verificación de sus algoritmos hasta 2 verificaciones si usa esta idea de agrupación. Pero ese 1 cheque adicional en el mejor de los casos vale la pena para ahorrar en el peor de los casos.
Nota: lo escribo en su totalidad en lugar de usar un bucle porque es más eficiente de esa manera.
fuente
¿Por qué no utilizar la búsqueda binaria ? Esto siempre se completará después de 5 operaciones (asumiendo un tamaño int de 4 bytes):
fuente
Otro método (división y búsqueda de módulos) merece una mención especial aquí desde el mismo enlace proporcionado por @ anton-tykhyy. este método es muy similar en rendimiento al método de multiplicación y búsqueda de DeBruijn con una diferencia leve pero importante.
división y búsqueda de módulos
La división de módulo y el método de búsqueda devuelven valores diferentes para v = 0x00000000 yv = FFFFFFFF, mientras que el método de multiplicación y búsqueda de DeBruijn devuelve cero en ambas entradas.
prueba:-
fuente
mod
es lento. En su lugar, puede utilizar el método de multiplicar-y-lookup original y restar!v
a partirr
de manejar los casos extremos.Según la página BitScan de programación de ajedrez y mis propias medidas, restar y xor es más rápido que negar y enmascarar.
(Tenga en cuenta que si va a contar los ceros finales
0
, el método que tengo regresa63
mientras que la negación y la máscara regresan0
).Aquí hay una resta y xor de 64 bits:
Como referencia, aquí hay una versión de 64 bits del método de negar y enmascarar:
fuente
(v ^ (v-1))
funciona proporcionadov != 0
. En caso dev == 0
que devuelva 0xFF .... FF while(v & -v)
da cero (que por cierto también es incorrecto, buf al menos conduce a un resultado razonable).v ^ (v-1)
, por lo que no es posible diferenciarlos. En mi escenario, nunca se ingresará cero.Puede comprobar si está configurado alguno de los bits de orden inferior. Si es así, observe el orden inferior de los bits restantes. p.ej,:
32bit int: compruebe si alguno de los primeros 16 está configurado. Si es así, compruebe si alguno de los primeros 8 está configurado. si es así, ....
si no es así, compruebe si alguno de los 16 superiores está configurado.
Esencialmente es una búsqueda binaria.
fuente
Vea mi respuesta aquí para saber cómo hacerlo con una sola instrucción x86, excepto que para encontrar el bit establecido menos significativo, querrá la
BSF
instrucción ("bit scan forward") en lugar de la que seBSR
describe allí.fuente
Otra solución, posiblemente no la más rápida, pero parece bastante buena.
Al menos no tiene ramas. ;)
fuente
1
s del 1 menos significativo a LSB, utilice((x & -x) - 1) << 1
en su lugarx ^ (x-1)
El 50% de todos los números volverán a aparecer en la primera línea de código.
El 75% de todos los números se devolverán en las primeras 2 líneas de código.
El 87% de todos los números volverán en las primeras 3 líneas de código.
El 94% de todos los números volverán en las primeras 4 líneas de código.
El 97% de todos los números volverán en las primeras 5 líneas de código.
etc.
Creo que las personas que se quejan de cuán ineficiente es el peor de los casos para este código no entienden cuán rara sucederá esa condición.
fuente
Encontré este ingenioso truco usando 'máscaras mágicas' en "El arte de programar, parte 4", que lo hace en tiempo O (log (n)) para un número de n bits. [con log (n) espacio extra]. Las soluciones típicas que verifican el bit establecido son O (n) o necesitan O (n) espacio adicional para una tabla de consulta, por lo que este es un buen compromiso.
Máscaras mágicas:
Idea clave: No de ceros finales en x = 1 * [(x & m0) = 0] + 2 * [(x & m1) = 0] + 4 * [(x & m2) = 0] + ...
fuente
Si C ++ 11 está disponible para usted, un compilador a veces puede hacer la tarea por usted :)
El resultado es un índice basado en 1.
fuente
ffs()
en tiempo de compilación, por lo que no es necesario usar esto para que funcione la propagación constante. (No tiene que evitar en línea-ASM, por supuesto.) Si realmente necesita algo que funciona como C ++ 11constexpr
, todavía se puede usar GNU C__builtin_ffs
.Esto es en lo que respecta a la respuesta de @Anton Tykhyy
Aquí está mi implementación de constexpr de C ++ 11 eliminando las conversiones y eliminando una advertencia en VC ++ 17 al truncar un resultado de 64 bits a 32 bits:
Para solucionar el problema de 0x1 y 0x0 que devuelven 0, puede hacer lo siguiente:
pero si el compilador no puede o no preprocesa la llamada, agregará un par de ciclos al cálculo.
Finalmente, si está interesado, aquí hay una lista de afirmaciones estáticas para verificar que el código hace lo que se pretende:
fuente
Aquí hay una alternativa simple, aunque encontrar registros es un poco costoso.
fuente
Recientemente, vi que el primer ministro de Singapur publicó un programa que escribió en Facebook, hay una línea para mencionarlo.
La lógica es simplemente "valor & -valor", suponga que tiene 0x0FF0, luego, 0FF0 & (F00F + 1), que es igual a 0x0010, eso significa que el 1 más bajo está en el cuarto bit .. :)
fuente
Si tiene los recursos, puede sacrificar memoria para mejorar la velocidad:
Nota: Esta tabla consumiría al menos 4 GB (16 GB si dejamos el tipo de retorno como
unsigned
). Este es un ejemplo de intercambio de un recurso limitado (RAM) por otro (velocidad de ejecución).Si su función necesita permanecer portátil y ejecutarse lo más rápido posible a cualquier costo, este sería el camino a seguir. En la mayoría de las aplicaciones del mundo real, una mesa de 4 GB no es realista.
fuente
:)
@Dan: Tienes razón sobre el almacenamiento en caché de la memoria. Vea el comentario de Mikeage arriba.