¿Cuál es tu técnica favorita de bit-wise? [cerrado]

14

Hace unos días, el miembro de StackExchange Anto preguntó acerca de los usos válidos para los operadores de bit-wise. Dije que cambiar era más rápido que multiplicar y dividir enteros por potencias de dos. El miembro de StackExchange, Daemin, respondió diciendo que el desplazamiento a la derecha presentaba problemas con números negativos.

En ese momento, nunca había pensado realmente en usar los operadores de cambio con enteros con signo. Principalmente utilicé esta técnica en el desarrollo de software de bajo nivel; por lo tanto, siempre usé enteros sin signo. C realiza cambios lógicos en enteros sin signo. No se presta atención al bit de signo cuando se realiza un cambio lógico a la derecha. Los bits vaciados se rellenan con ceros. Sin embargo, C realiza una operación de desplazamiento aritmético cuando desplaza un entero con signo a la derecha. Los bits desocupados se llenan con el bit de signo. Esta diferencia hace que un valor negativo se redondee hacia el infinito en lugar de truncarse hacia cero, que es un comportamiento diferente al de la división entera con signo.

Unos pocos minutos de reflexión dieron como resultado una solución de primer orden. La solución convierte condicionalmente los valores negativos en valores positivos antes de cambiar. Un valor se convierte condicionalmente de nuevo a su forma negativa después de que se haya realizado la operación de cambio.

int a = -5;
int n = 1;

int negative = q < 0; 

a = negative ? -a : a; 
a >>= n; 
a = negative ? -a : a; 

El problema con esta solución es que las instrucciones de asignación condicional generalmente se traducen en al menos una instrucción de salto, y las instrucciones de salto pueden ser costosas en procesadores que no decodifican ambas rutas de instrucciones. Tener que volver a cebar una tubería de instrucciones dos veces hace una buena mella en cualquier ganancia de rendimiento obtenida al cambiar sobre la división.

Dicho lo anterior, me desperté el sábado con la respuesta al problema de asignación condicional. El problema de redondeo que experimentamos al realizar una operación de cambio aritmético solo ocurre cuando trabajamos con la representación del complemento a dos. No ocurre con la representación del complemento de uno. La solución al problema consiste en convertir el valor del complemento de dos en el valor del complemento de uno antes de realizar la operación de cambio. Luego tenemos que convertir el valor del complemento de uno nuevamente al valor del complemento de dos. Sorprendentemente, podemos realizar este conjunto de operaciones sin convertir condicionalmente los valores negativos antes de realizar la operación de cambio.

int a = -5;
int n = 1;

register int sign = (a >> INT_SIZE_MINUS_1) & 1

a = (a - sign) >> n + sign;   

El valor negativo del complemento de dos se convierte en el valor negativo del complemento de uno restando uno. Por otro lado, el valor negativo del complemento de uno se convierte en el valor negativo del complemento de dos al sumar uno. El código mencionado anteriormente funciona porque el bit de signo se usa para convertir del complemento de dos al complemento de uno y viceversa . Solo los valores negativos tendrán sus bits de signo establecidos; por lo tanto, el signo variable será igual a cero cuando a es positivo.

Dicho lo anterior, ¿puedes pensar en otros trucos sabios como el anterior que se han convertido en tu bolsa de trucos? ¿Cuál es tu truco favorito de bit-wise? Siempre estoy buscando nuevos trucos orientados al rendimiento basados ​​en bits.

bit-twiddler
fuente
3
Esta pregunta y su nombre de cuenta: el mundo tiene sentido nuevamente ...
JK
+1 Pregunta interesante como seguimiento a la mía y de otra manera también;)
Anto
También hice algunos cálculos rápidos de paridad una vez. La paridad es un poco molesta porque tradicionalmente involucra bucles y contar si se establece un poco, todo lo cual requiere muchos saltos. La paridad se puede calcular usando shift y XOR, luego un montón de los que se hacen uno tras otro evita los bucles y saltos.
rapid_now
2
¿Eres consciente de que hay un libro completo sobre estas técnicas? - Hackers Delight amazon.com/Hackers-Delight-Henry-S-Warren/dp/0201914654
nikie
Sí, también hay un sitio web dedicado a las operaciones con bits. Olvidé la URL, pero Google la mostrará pronto.
rapid_now

Respuestas:

23

Me encanta el truco de Gosper (HAKMEM # 175), una forma muy astuta de tomar un número y obtener el siguiente número con el mismo número de bits establecido. Es útil, por ejemplo, para generar combinaciones de kelementos de esta nmanera:

int set = (1 << k) - 1;
int limit = (1 << n);
while (set < limit) {
    doStuff(set);

    // Gosper's hack:
    int c = set & -set;
    int r = set + c;
    set = (((r^set) >>> 2) / c) | r;
}
Peter Taylor
fuente
77
+1. Pero a partir de ahora, tendré pesadillas sobre encontrar esta durante una sesión de depuración sin el comentario.
nikie
@nikie, muahahahaha! (Tiendo a usar esto para cosas como los problemas del Proyecto Euler: mi trabajo diario no implica mucha combinatoria).
Peter Taylor
7

El método de raíz cuadrada inversa rápida utiliza las técnicas de nivel de bits más extrañas para calcular la inversa de una raíz cuadrada que he visto:

float Q_rsqrt( float number )
{
    long i;
    float x2, y;
    const float threehalfs = 1.5F;

    x2 = number * 0.5F;
    y  = number;
    i  = * ( long * ) &y;                       // evil floating point bit level hacking [sic]
    i  = 0x5f3759df - ( i >> 1 );               // what the fuck? [sic]
    y  = * ( float * ) &i;
    y  = y * ( threehalfs - ( x2 * y * y ) );   // 1st iteration
    //    y  = y * ( threehalfs - ( x2 * y * y ) );   // 2nd iteration, this can be removed

    return y;
}
gablin
fuente
Fast sqrt también es increíble. Carmack parece ser uno de los mejores programadores.
BenjaminB
Wikipedia tiene fuentes aún más antiguas, por ejemplo beyond3d.com/content/articles/15
MSalters
0

División por 3: sin recurrir a una llamada a la biblioteca en tiempo de ejecución.

Resulta que la división por 3 (gracias a una pista sobre el desbordamiento de pila) se puede aproximar como:

X / 3 = [(x / 4) + (x / 12)]

Y X / 12 es (x / 4) / 3. Hay un elemento de recursión que aparece repentinamente aquí.

También resulta que si limita el rango de los números en los que está jugando, puede limitar el número de iteraciones necesarias.

Y por lo tanto, para enteros sin signo <2000, el siguiente es un algoritmo rápido y simple / 3. (Para números más grandes, solo agregue más pasos). Los compiladores optimizan el heck de esto para que sea rápido y pequeño:

FastDivide3 estático sin signo corto (const arg corto sin signo)
{
  unsigned short RunningSum;
  unsigned short fracctionthwelth;

  RunningSum = arg >> 2;

  FractionalTwelth = RunningSum >> 2;
  RunningSum + = FractionalTwelth;

  Fraccional Doce >> = 2;
  RunningSum + = FractionalTwelth;

  Fraccional Doce >> = 2;
  RunningSum + = FractionalTwelth;

  Fraccional Doce >> = 2;
  RunningSum + = FractionalTwelth;

  // Más repeticiones de las 2 líneas anteriores para mayor precisión.

  volver RunningSum;
}
rápidamente_ahora
fuente
1
Por supuesto, esto solo es relevante en microcontroladores muy oscuros. Cualquier CPU real fabricada en las últimas dos décadas no necesita una biblioteca en tiempo de ejecución para la división de enteros.
MSalters
1
Claro, pero los micros pequeños sin un multiplicador de hardware son realmente muy comunes. Y si trabaja en terrenos empotrados y quiere ahorrar $ 0.10 en cada uno de un millón de productos vendidos, entonces es mejor que conozca algunos trucos sucios. Ese dinero ahorrado = beneficio adicional que hace que tu jefe esté muy feliz.
rápidamente_abr
Bueno, sucio ... solo se multiplica por .0101010101(aprox. 1/3). Pro consejo: también se puede multiplicar por .000100010001y 101(que tarda sólo 3 bitshifts, sin embargo, tiene la mejor aproximación.010101010101
MSalters
¿Cómo podría hacer eso solo con enteros y sin coma flotante?
rapid_now
1
A nivel de bits, x * 101 = x + x << 2. Del mismo modo, x * 0.000100010001 es x >> 4 + x >> 8 + x >> 12.
MSalters