¿Cuál es el algoritmo más eficiente para lograr lo siguiente?
0010 0000 => 0000 0100
La conversión es de MSB-> LSB a LSB-> MSB. Todos los bits deben invertirse; es decir, esto no es intercambio de endianness.
c
algorithm
bit-manipulation
green_t
fuente
fuente
Respuestas:
NOTA : Todos los algoritmos a continuación están en C, pero deberían ser portátiles para el idioma de su elección (simplemente no me mire cuando no sean tan rápidos :)
Opciones
Memoria baja (
int
máquina de 32 bits , 32 bits) (desde aquí ):De la famosa página de Bit Twiddling Hacks :
Más rápido (tabla de búsqueda) :
Puede ampliar esta idea a 64 bits
int
, o cambiar la memoria por velocidad (suponiendo que su caché de datos L1 sea lo suficientemente grande) e invertir 16 bits a la vez con una tabla de búsqueda de 64K.Otros
Sencillo
Más rápido (procesador de 32 bits)
Más rápido (procesador de 64 bits)
Si desea hacer esto en un bit de 32 bits
int
, simplemente invierta los bits en cada byte e invierta el orden de los bytes. Es decir:Resultados
Hice una evaluación comparativa de las dos soluciones más prometedoras, la tabla de búsqueda y bit-AND (la primera). La máquina de prueba es una computadora portátil con 4GB de DDR2-800 y un Core 2 Duo T7500 @ 2.4GHz, 4MB L2 Cache; YMMV. Solía gcc 4.3.2 en Linux de 64 bits. OpenMP (y los enlaces GCC) se utilizaron para temporizadores de alta resolución.
reverse.c
reverse_lookup.c
Probé ambos enfoques con varias optimizaciones diferentes, ejecuté 3 ensayos en cada nivel y cada ensayo revirtió 100 millones al azar
unsigned ints
. Para la opción de tabla de búsqueda, probé ambos esquemas (opciones 1 y 2) que figuran en la página de hacks bit a bit. Los resultados se muestran a continuación.Bitwise Y
Tabla de búsqueda (opción 1)
Tabla de búsqueda (opción 2)
Conclusión
Use la tabla de búsqueda, con la opción 1 (el direccionamiento de bytes es sorprendentemente lento) si le preocupa el rendimiento. Si necesita exprimir hasta el último byte de memoria de su sistema (y podría, si le preocupa el rendimiento de la inversión de bits), las versiones optimizadas del enfoque bit a bit Y tampoco son demasiado malas.
Consideración
Sí, sé que el código de referencia es un truco completo. Las sugerencias sobre cómo mejorarlo son más que bienvenidas. Cosas que sé sobre:
ld
explotó con algún error de redefinición de símbolo loco), por lo que no creo que el código generado esté ajustado para mi microarquitectura.32 bits
EDITAR: También intenté usar
uint64_t
tipos en mi máquina para ver si hubo algún aumento en el rendimiento. El rendimiento fue aproximadamente un 10% más rápido que el de 32 bits, y fue casi idéntico tanto si estaba utilizando tipos de 64 bits para invertir bits en dosint
tipos de 32 bits a la vez, como si realmente estaba invirtiendo bits en la mitad de 64- valores de bit El código de ensamblaje se muestra a continuación (para el caso anterior, invirtiendo bits para dosint
tipos de 32 bits a la vez):fuente
Este hilo me llamó la atención ya que trata un problema simple que requiere mucho trabajo (ciclos de CPU) incluso para una CPU moderna. Y un día también estuve allí con el mismo problema ¤ #% "#". Tuve que voltear millones de bytes. Sin embargo, sé que todos mis sistemas de destino son modernos basados en Intel, ¡así que comencemos a optimizar al extremo!
Así que usé el código de búsqueda de Matt J como base. El sistema que estoy evaluando es un i7 haswell 4700eq.
El bitflipping de búsqueda de Matt J 400 000 000 bytes: alrededor de 0.272 segundos.
Luego seguí adelante e intenté ver si el compilador ISPC de Intel podía vectorizar la aritmética en el reverso. C.
No voy a aburrirlos con mis hallazgos aquí, ya que intenté mucho para ayudar al compilador a encontrar cosas, de todos modos terminé con un rendimiento de alrededor de 0,15 segundos para bitflip 400 000 000 bytes. Es una gran reducción, pero para mi aplicación todavía es demasiado lenta.
Entonces, la gente me deja presentar el bitflipper basado en Intel más rápido del mundo. Reloj a:
Tiempo de bitflip 400000000 bytes: 0.050082 segundos !!!!!
Los printf son para depurar ...
Aquí está el caballo de batalla:
El código toma 32 bytes y luego enmascara los nibbles. El mordisco alto se desplaza a la derecha por 4. Luego uso vpshufb y ymm4 / ymm3 como tablas de búsqueda. Podría usar una sola tabla de búsqueda, pero luego tendría que desplazarme a la izquierda antes de ORar los mordiscos juntos nuevamente.
Hay formas aún más rápidas de voltear los bits. Pero estoy obligado a un solo hilo y CPU, así que esto fue lo más rápido que pude lograr. ¿Puedes hacer una versión más rápida?
No haga comentarios sobre el uso de los comandos equivalentes intrínsecos del compilador Intel C / C ++ ...
fuente
pshub
, porque después de todo, ¡la mejor cuenta también se hace con ella! Lo hubiera escrito aquí si no fuera por ti. Prestigio.popcnt
,tzcnt
ypext
todos en el puerto 1. Por lo tanto, cada unopext
o letzcnt
cuesta unpopcnt
rendimiento. Si sus datos están calientes en el caché L1D, la forma más rápida de contar una matriz en las CPU Intel es con AVX2 pshufb. (Ryzen tiene unpopcnt
rendimiento de 4 por reloj, por lo que probablemente sea óptimo, pero la familia Bulldozer tiene unpopcnt r64,r64
rendimiento de cada 4 relojes ... agner.org/optimize ).Esta es otra solución para las personas que aman la recursividad.
La idea es simple. Divida la entrada por la mitad e intercambie las dos mitades, continúe hasta que llegue a un solo bit.
Aquí hay una función recursiva para resolverlo. (Tenga en cuenta que he usado ints sin signo, por lo que puede funcionar para entradas de hasta sizeof (unsigned int) * 8 bits.
Esta es la salida:
fuente
numBits
es int, cuando divide 3 por 2 para la función param se redondeará a 1?Bueno, esto ciertamente no será una respuesta como la de Matt J, pero espero que siga siendo útil.
Esta es exactamente la misma idea que el mejor algoritmo de Matt, excepto que hay una pequeña instrucción llamada BSWAP que intercambia los bytes (no los bits) de un número de 64 bits. Entonces b7, b6, b5, b4, b3, b2, b1, b0 se convierte en b0, b1, b2, b3, b4, b5, b6, b7. Dado que estamos trabajando con un número de 32 bits, necesitamos cambiar nuestro número de intercambio de bytes hacia abajo 32 bits. ¡Esto nos deja con la tarea de intercambiar los 8 bits de cada byte que está hecho y listo! Ya hemos terminado.
Tiempo: en mi máquina, el algoritmo de Matt se ejecutó en ~ 0.52 segundos por prueba. La mía corrió en aproximadamente 0,42 segundos por prueba. 20% más rápido no está mal, creo.
Si le preocupa la disponibilidad de la instrucción BSWAP Wikipedia enumera la instrucción BSWAP que se agregó con 80846 que salió en 1989. Cabe señalar que Wikipedia también afirma que esta instrucción solo funciona en registros de 32 bits, lo que claramente no es caso en mi máquina, funciona mucho solo en registros de 64 bits.
Este método funcionará igualmente bien para cualquier tipo de datos integral, por lo que el método puede generalizarse trivialmente pasando el número de bytes deseado:
que luego se puede llamar como:
El compilador debería poder optimizar el parámetro adicional (suponiendo que el compilador incorpore la función) y para el
sizeof(size_t)
caso, el desplazamiento a la derecha se eliminaría por completo. Tenga en cuenta que GCC al menos no puede eliminar el BSWAP y el desplazamiento a la derecha si se apruebasizeof(char)
.fuente
unsigned long long int
que deben tener al menos 64 bits, según aquí y aquíLa respuesta de Anders Cedronius ofrece una gran solución para las personas que tienen una CPU x86 con soporte AVX2. Para plataformas x86 sin soporte AVX o plataformas que no sean x86, cualquiera de las siguientes implementaciones debería funcionar bien.
El primer código es una variante del método clásico de particionamiento binario, codificado para maximizar el uso de la expresión shift-plus-logic útil en varios procesadores ARM. Además, utiliza la generación de máscaras sobre la marcha que podría ser beneficiosa para los procesadores RISC que, de lo contrario, requieren múltiples instrucciones para cargar cada valor de máscara de 32 bits. Los compiladores para plataformas x86 deben usar propagación constante para calcular todas las máscaras en tiempo de compilación en lugar de tiempo de ejecución.
En el volumen 4A de "El arte de la programación de computadoras", D. Knuth muestra formas inteligentes de revertir bits que sorprendentemente requieren menos operaciones que los algoritmos clásicos de partición binaria. Uno de estos algoritmos para operandos de 32 bits, que no puedo encontrar en TAOCP, se muestra en este documento en el sitio web de Hacker's Delight.
Usando el compilador Intel C / C ++ 13.1.3.198, ambas funciones anteriores auto-vectorizan bien los
XMM
registros de destino . También se pueden vectorizar manualmente sin mucho esfuerzo.En mi IvyBridge Xeon E3 1270v2, usando el código auto-vectorizado,
uint32_t
se invirtieron 100 millones de palabras en 0.070 segundos usandobrev_classic()
, y 0.068 segundos usandobrev_knuth()
. Me aseguré de asegurar que mi punto de referencia no estuviera limitado por el ancho de banda de la memoria del sistema.fuente
brev_knuth()
? La atribución en el PDF de Hacker's Delight parece indicar que estos números son directamente del propio Knuth. No puedo afirmar que he entendido la descripción de Knuth de los principios de diseño subyacentes en TAOCP lo suficiente como para explicar cómo se derivaron las constantes, o cómo se abordarían las constantes derivadas y los factores de cambio para tamaños de palabras arbitrarios.Suponiendo que tiene una matriz de bits, ¿qué tal esto? 1. Comenzando desde MSB, inserte los bits en una pila uno por uno. 2. Haga estallar los bits de esta pila en otra matriz (o la misma matriz si desea ahorrar espacio), colocando el primer bit reventado en MSB y pasando a bits menos significativos desde allí.
fuente
La instrucción ARM nativa "rbit" puede hacerlo con 1 ciclo de CPU y 1 registro de CPU adicional, imposible de superar.
fuente
¡Esto no es trabajo para un humano! ... pero perfecto para una máquina
Esto es 2015, 6 años después de que se hizo esta pregunta por primera vez. Los compiladores se han convertido en nuestros maestros y nuestro trabajo como humanos es solo ayudarlos. Entonces, ¿cuál es la mejor manera de dar nuestras intenciones a la máquina?
La inversión de bits es tan común que debe preguntarse por qué el ISA cada vez mayor del x86 no incluye instrucciones para hacerlo de una vez.
La razón: si le das tu verdadera intención concisa al compilador, la inversión de bits solo debería tomar ~ 20 ciclos de CPU . Déjame mostrarte cómo crear reverse () y usarlo:
Compilar este programa de muestra con la versión Clang> = 3.6, -O3, -march = native (probado con Haswell), proporciona un código de calidad de diseño utilizando las nuevas instrucciones AVX2, con un tiempo de ejecución de 11 segundos procesando ~ 1 billón de reverse () s. Eso es ~ 10 ns por marcha atrás (), con un ciclo de CPU de .5 ns, suponiendo que 2 GHz nos coloca en los 20 ciclos de CPU.
Advertencia: este código de muestra debería ser un punto de referencia decente durante algunos años, pero eventualmente comenzará a mostrar su antigüedad una vez que los compiladores sean lo suficientemente inteligentes como para optimizar main () para simplemente imprimir el resultado final en lugar de calcular realmente nada. Pero por ahora funciona en mostrar reverse ().
fuente
Bit-reversal is so common...
No se sobre eso. Trabajo con código que maneja datos a nivel de bits prácticamente todos los días, y no recuerdo haber tenido esta necesidad específica. ¿En qué escenarios lo necesitas? - No es que no sea un problema interesante de resolver por derecho propio.Por supuesto, la fuente obvia de los trucos de bit-twiddling está aquí: http://graphics.stanford.edu/~seander/bithacks.html#BitReverseObvious
fuente
Sé que no es C pero asm:
Esto funciona con el bit de transporte, por lo que también puede guardar banderas
fuente
rcl
cambiar CF avar1
, en lugar de soloshl
lo que no lee las banderas. (Oadc dx,dx
). Incluso con esa solución, ¡esto es ridículamente lento, usando lasloop
instrucciones lentas y guardandovar1
en la memoria! En realidad, creo que se supone que esto está produciendo la salida en AX, pero guarda / restaura el valor anterior de AX por encima del resultado.Implementación con poca memoria y más rápida.
fuente
Bueno, esto es básicamente lo mismo que el primer "reverse ()" pero es de 64 bits y solo necesita una máscara inmediata para cargarse desde el flujo de instrucciones. GCC crea código sin saltos, por lo que esto debería ser bastante rápido.
fuente
Tenía curiosidad por lo rápido que sería la rotación cruda obvia. En mi máquina (i7 @ 2600), el promedio de 1,500,150,000 iteraciones fue
27.28 ns
(sobre un conjunto aleatorio de 131,071 enteros de 64 bits).Ventajas: la cantidad de memoria necesaria es pequeña y el código es simple. Yo diría que tampoco es tan grande. El tiempo requerido es predecible y constante para cualquier entrada (128 operaciones SHIFT aritméticas + 64 operaciones lógicas AND + 64 operaciones lógicas OR).
Comparé el mejor momento obtenido por @Matt J, quien tiene la respuesta aceptada. Si leí su respuesta correctamente, lo mejor que obtuvo fue
0.631739
segundos para1,000,000
iteraciones, lo que lleva a un promedio de631 ns
por rotación.El fragmento de código que utilicé es el siguiente:
fuente
Es posible que desee utilizar la biblioteca de plantillas estándar. Puede ser más lento que el código mencionado anteriormente. Sin embargo, me parece más claro y más fácil de entender.
fuente
Genérico
Código C. Usando 1 byte de datos de entrada num como ejemplo.
fuente
¿Qué tal lo siguiente:
Pequeño y fácil (aunque solo de 32 bits).
fuente
Pensé que esta es una de las formas más simples de revertir el bit. por favor avíseme si hay algún defecto en esta lógica. Básicamente, en esta lógica, verificamos el valor del bit en posición. establezca el bit si el valor es 1 en posición invertida.
fuente
fuente
k
siempre es una potencia de 2, pero los compiladores probablemente no lo prueben y lo conviertan en bit-scan / shift.Creo que el método más simple que conozco sigue.
MSB
es entrada yLSB
es salida 'invertida':fuente
fuente
Otra solución basada en bucles que sale rápidamente cuando el número es bajo (en C ++ para múltiples tipos)
o en C para un int sin firmar
fuente
Parece que muchas otras publicaciones están preocupadas por la velocidad (es decir, mejor = más rápido). ¿Qué pasa con la simplicidad? Considerar:
y espero que ese compilador inteligente se optimice para usted.
Si desea invertir una lista más larga de bits (que contiene
sizeof(char) * n
bits), puede usar esta función para obtener:Esto revertiría [10000000, 10101010] en [01010101, 00000001].
fuente
ith_bit = (c >> i) & 1
. También guarde un SUB desplazando enreversed_char
lugar de desplazar el bit, a menos que espere que se compile en x86 asub something
/bts reg,reg
para establecer el enésimo bit en el registro de destino.Inversión de bits en pseudocódigo
fuente -> byte para revertir b00101100 destino -> revertir, también debe ser de tipo sin signo para que el bit de signo no se propague hacia abajo
copiar en temp para que el original no se vea afectado, también debe ser de tipo sin signo para que el bit de signo no se cambie automáticamente
LOOP8: // realiza esta prueba 8 veces si la bytecopy es <0 (negativa)
fuente
Mi simple solucion
fuente
i
? Además, ¿qué es esa constante mágica* 4
? EsCHAR_BIT / 2
?Esto es para 32 bits, necesitamos cambiar el tamaño si consideramos 8 bits.
Lectura del entero de entrada "num" en orden LSB-> MSB y almacenamiento en num_reverse en orden MSB-> LSB.
fuente
fuente