Extracción de bits con una sola multiplicación


Vi una técnica interesante utilizada en una respuestaa otra pregunta, y me gustaría entenderla un poco mejor.

Se nos da un entero de 64 bits sin signo, y estamos interesados en los siguientes bits:

1.......2.......3.......4.......5.......6.......7.......8.......

Específicamente, nos gustaría moverlos a las ocho primeras posiciones, así:

12345678........................................................

No nos importa el valor de los bits indicados por ., y no tienen que ser preservados.

La solución era enmascarar los bits no deseados, y multiplicar el resultado por 0x2040810204081. Esto, como resulta, hace el truco.

¿Qué tan general es este método? ¿Se puede utilizar esta técnica para extraer cualquier subconjunto de bits? Si no, ¿cómo se determina si el método funciona o no para un conjunto particular de bits?

Finalmente, ¿cómo se podría encontrar la (a?) multiplicador correcto para extraer los bits dados?

Author: Community, 2013-01-27

5 answers

Pregunta muy interesante, y truco inteligente.

Veamos un ejemplo simple de cómo manipular un solo byte. Usando unsigned 8 bit para simplificar. Imagina que tu número es xxaxxbxx y quieres ab000000.

La solución consistía en dos pasos: un poco de enmascaramiento, seguido de multiplicación. La máscara de bits es una operación simple y que convierte bits sin interés en ceros. En el caso anterior, su máscara sería 00100100 y el resultado 00a00b00.

Ahora la parte difícil: convirtiendo eso en ab.......

Una multiplicación es un montón de operaciones de cambio y suma. La clave es permitir que overflow "aleje" los bits que no necesitamos y coloque los que queremos en el lugar correcto.

La multiplicación por 4 (00000100) cambiaría todo lo que queda por 2 y te llevaría a a00b0000. Para que el b se mueva hacia arriba necesitamos multiplicar por 1 (para mantener la a en el lugar correcto) + 4 (para mover la b hacia arriba). Esta suma es 5, y combinado con el anterior 4 obtenemos un número mágico de 20, o 00010100. El original fue 00a00b00 después de enmascarar; la multiplicación da:

000000a00b000000
00000000a00b0000 +
----------------
000000a0ab0b0000
xxxxxxxxab......

Desde este enfoque se puede extender a números más grandes y más bits.

Una de las preguntas que hizo fue "¿se puede hacer esto con cualquier número de bits?"Creo que la respuesta es "no", a menos que permitas varias operaciones de enmascaramiento, o varias multiplicaciones. El problema es la cuestión de las" colisiones "- por ejemplo, la" perdida b " en el problema anterior. Imagina que tenemos que hacer esto a un número como xaxxbxxcx. Siguiendo el enfoque anterior, pensarías que necesitamos {x 2, x {1 + 4 + 16}} = x 42 (oooh - la respuesta a todo!). Resultado:

00000000a00b00c00
000000a00b00c0000
0000a00b00c000000
-----------------
0000a0ababcbc0c00
xxxxxxxxabc......

Como puedes ver, todavía funciona, pero "solo". La clave aquí es que hay "suficiente espacio" entre los bits que queremos que podamos exprimir todo. No podría agregar un cuarto bit d justo después de c, porque obtendría instancias donde obtengo c + d, bits podrían llevar, ...

Así que sin pruebas formales, yo respondería más partes interesantes de su pregunta como sigue: "No, esto no funcionará para cualquier número de bits. Para extraer N bits, necesita (N-1) espacios entre los bits que desea extraer, o tener pasos adicionales de multiplicación de máscara."

La única excepción que se me ocurre para la regla "debe tener (N-1) ceros entre bits" es esta: si desea extraer dos bits que son adyacentes entre sí en el original, Y desea mantenerlos en el mismo orden, entonces todavía puede hacerlo. Y con el propósito de la regla (N-1) cuentan como dos bits.

Hay otro insight - inspirado por la respuesta de @Ternary a continuación (ver mi comentario allí). Para cada bit interesante, solo necesita tantos ceros a la derecha como espacio para los bits que necesitan ir allí. Pero también, necesita tantos bits a la izquierda como tiene bits de resultado a la izquierda. Así que si un bit b termina en la posición m de n, entonces necesita tener m-1 ceros a su izquierda y n-m ceros a su derecha. Especialmente cuando los bits no están en el mismo orden en el número original como lo serán después del reordenamiento, esto es una mejora importante para los criterios originales. Esto significa, por ejemplo, que una palabra de 16 bits

a...e.b...d..c..

Se puede desplazar a

abcde...........

Aunque solo hay un espacio entre e y b, dos entre d y c, tres entre los otros. ¿Qué pasó con N-1?? En este caso, a...e se convierte en "un bloque" - se multiplican por 1 para terminar en el lugar correcto, y así "tenemos e gratis". El lo mismo es cierto para b y d (b necesita tres espacios a la derecha, d necesita los mismos tres a su izquierda). Así que cuando calculamos el número mágico, encontramos que hay duplicados:

a: << 0  ( x 1    )
b: << 5  ( x 32   )
c: << 11 ( x 2048 )
d: << 5  ( x 32   )  !! duplicate
e: << 0  ( x 1    )  !! duplicate

Claramente, si quisieras estos números en un orden diferente, tendrías que espaciarlos más. Podemos reformular la regla (N-1): "Siempre funcionará si hay al menos (N-1) espacios entre bits; o, si se conoce el orden de bits en el resultado final, entonces si un bit b termina en la posición m de n, necesita tiene m - 1 ceros a su izquierda, y n-m ceros a su derecha."

@Ternary señaló que esta regla no funciona del todo, ya que puede haber un carry de bits agregando "justo a la derecha del área de destino", es decir, cuando los bits que estamos buscando son todos unos. Continuando con el ejemplo que di arriba con los cinco bits apretados en una palabra de 16 bits: si comenzamos con

a...e.b...d..c..

Para simplificar, nombraré las posiciones de bits ABCDEFGHIJKLMNOP

Las matemáticas que íbamos a hacer fue

ABCDEFGHIJKLMNOP

a000e0b000d00c00
0b000d00c0000000
000d00c000000000
00c0000000000000 +
----------------
abcded(b+c)0c0d00c00

Hasta ahora, pensábamos que cualquier cosa por debajo de abcde (posiciones ABCDE) no importaría, pero de hecho, como señaló @Ternary, si b=1, c=1, d=1 entonces (b+c) en posición G causará que un bit se lleve a la posición F, lo que significa que (d+1) en posición F llevará un bit a E - y nuestro resultado se estropeará. Tenga en cuenta que el espacio a la derecha del bit menos significativo de interés (c en este ejemplo) no importa, ya que la multiplicación causará relleno con ceros de beyone la parte menos significativa.

Así que necesitamos modificar nuestra regla (m-1)/(n-m). Si hay más de un bit que tiene " exactamente (n-m) bits no utilizados a la derecha (sin contar el último bit en el patrón - "c" en el ejemplo anterior), entonces necesitamos fortalecer la regla - y tenemos que hacerlo iterativamente!

Tenemos que mirar no solo el número de bits que cumplen el criterio (n-m), sino también los que están en (n-m+1), etc. Vamos a llamar a su número Q0 (exactamente n-m a siguiente bit), Q1 (n-m + 1), hasta Q(N-1) (n-1). Entonces corremos el riesgo de llevar si

Q0 > 1
Q0 == 1 && Q1 >= 2
Q0 == 0 && Q1 >= 4
Q0 == 1 && Q1 > 1 && Q2 >=2
... 

Si miras esto, puedes ver que si escribes una expresión matemática simple{[38]]}

W = N * Q0 + (N - 1) * Q1 + ... + Q(N-1)

Y el resultado es W > 2 * N, entonces necesita aumentar el criterio RHS en un bit a (n-m+1). En este punto, la operación es segura siempre y cuando W < 4; si eso no funciona, aumente el criterio uno más, etc.

Creo que seguir lo anterior te llevará un largo camino a tu respuesta...

 230
Author: Floris,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-04-20 05:31:12

Pregunta muy interesante. Estoy interviniendo con mis dos centavos, que es que, si puedes manejar problemas como este en términos de lógica de primer orden sobre la teoría de bitvector, entonces los probadores de teoremas son tu amigo, y potencialmente pueden proporcionarte respuestas muy rápidas a tus preguntas. Vamos a re-establecer el problema que se pregunta como un teorema:

" Existen algunas constantes de 64 bits 'mask' y 'multiplicand' tales que, para todos los vectores de bits de 64 bits x, en la expresión y =(x & mask) * multiplicand / multiplicando, tenemos que y.63 == x.63, y.62 == x.55, y.61 == x.47, etc."

Si esta oración es de hecho un teorema, entonces es cierto que algunos valores de las constantes 'mask' y 'multiplicand' satisfacen esta propiedad. Así que vamos a expresar esto en términos de algo que un probador de teoremas puede entender, a saber, SMT-LIB 2 entrada:

(set-logic BV)

(declare-const mask         (_ BitVec 64))
(declare-const multiplicand (_ BitVec 64))

(assert
  (forall ((x (_ BitVec 64)))
    (let ((y (bvmul (bvand mask x) multiplicand)))
      (and
        (= ((_ extract 63 63) x) ((_ extract 63 63) y))
        (= ((_ extract 55 55) x) ((_ extract 62 62) y))
        (= ((_ extract 47 47) x) ((_ extract 61 61) y))
        (= ((_ extract 39 39) x) ((_ extract 60 60) y))
        (= ((_ extract 31 31) x) ((_ extract 59 59) y))
        (= ((_ extract 23 23) x) ((_ extract 58 58) y))
        (= ((_ extract 15 15) x) ((_ extract 57 57) y))
        (= ((_ extract  7  7) x) ((_ extract 56 56) y))
      )
    )
  )
)

(check-sat)
(get-model)

Y ahora vamos a preguntar al probador de teoremas Z3 si esto es un teorema:

z3.exe /m /smt2 ExtractBitsThroughAndWithMultiplication.smt2

El resultado es:

sat
(model
  (define-fun mask () (_ BitVec 64)
    #x8080808080808080)
  (define-fun multiplicand () (_ BitVec 64)
    #x0002040810204081)
)

Bingo! Se reproduce el resultado dado en el post original en 0.06 segundos.

Mirando esto desde una perspectiva más general, podemos ver esto como una instancia de un problema de síntesis de programas de primer orden, que es un área incipiente de investigación sobre la cual se han publicado pocos artículos. Una búsqueda de "program synthesis" filetype:pdf debería ayudarte a empezar.

 152
Author: Syzygy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-03-20 13:09:36

Cada 1-bit en el multiplicador se utiliza para copiar uno de los bits en su posición correcta:

  • 1 ya está en la posición correcta, así que multiplique por 0x0000000000000001.
  • 2 debe ser desplazado 7 posiciones de bits a la izquierda, por lo que se multiplica por 0x0000000000000080 (bit 7 se establece).
  • 3 debe ser desplazado 14 posiciones de bits a la izquierda, por lo que se multiplica por 0x0000000000000400 (bit 14 se establece).
  • y así sucesivamente hasta
  • 8 debe ser desplazado 49 posiciones de bits a la izquierda, por lo que multiplicamos por 0x0002000000000000 (bit 49 está establecido).

El multiplicador es la suma de los multiplicadores para los bits individuales.

Esto solo funciona porque los bits que se recopilan no están demasiado juntos, de modo que la multiplicación de bits que no pertenecen juntos en nuestro esquema o caen más allá del bit 64 o en la parte inferior no importa.

Tenga en cuenta que los otros bits en el número original deben ser 0. Esto se puede lograr enmascarándolos con una operación Y.

 84
Author: starblue,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-01-29 21:14:52

(Nunca lo había visto antes. Este truco es genial!)

Ampliaré un poco la afirmación de Floris de que al extraer n bits necesita n-1 espacio entre cualquier bits no consecutivos:

Mi pensamiento inicial (veremos en un minuto cómo esto no funciona del todo) fue que podría hacerlo mejor: Si desea extraer n bits, tendrá una colisión al extraer / desplazar bits i si tiene alguien (no consecutivo con bit i) en los i-1 bits anteriores o n-i bits posteriores.

Voy a dar algunos ejemplos para ilustrar: {[45]]}

...a..b...c... Funciona (nadie en los 2 bits después de a, el bit antes y el bit después de b, y nadie está en los 2 bits antes c):

  a00b000c
+ 0b000c00
+ 00c00000
= abc.....

...a.b....c... Falla porque b está en los 2 bits después de a (y es arrastrado al lugar de otra persona cuando cambiamos a):

  a0b0000c
+ 0b0000c0
+ 00c00000
= abX.....

...a...b.c... Falla porque b está en los 2 bits que preceden a c (y es empujado al lugar de otra persona cuando nosotros nos desplazamos c):

  a000b0c0
+ 0b0c0000
+ b0c00000
= Xbc.....

...a...bc...d... Funciona porque los bits consecutivos se desplazan juntos:

  a000bc000d
+ 0bc000d000
+ 000d000000
= abcd000000

Pero tenemos un problema. Si usamos n-i en lugar de n-1 podríamos tener el siguiente escenario: ¿qué pasa si tenemos una colisión fuera de la parte que nos interesa, algo que podría enmascarar al final, pero cuya llevar bits acaban interfiriendo en la importante de la onu-enmascarado gama? (y nota: el requisito n-1 se asegura de que esto no suceda asegurándose de que los bits i-1 después de que nuestro rango no enmascarado esté claro cuando cambiamos el ith bit)

...a...b..c...d... Potencial fallo en carry-bits, c está en n-1 después de b, pero satisface n-i criterios:

  a000b00c000d
+ 0b00c000d000
+ 00c000d00000
+ 000d00000000
= abcdX.......

Entonces, ¿por qué no volvemos a ese requisito de "n-1 bits de espacio"? Porque podemos hacerlo mejor:

...a....b..c...d.. Falla la prueba "n-1 bits de espacio", pero funciona para nuestro truco de extracción de bits:

+ a0000b00c000d00
+ 0b00c000d000000
+ 00c000d00000000
+ 000d00000000000
= abcd...0X......

No se me ocurre una buena manera de caracterizar estos campos que no tienen n-1 espacio entre bits importantes, pero aún así funcionarían para nuestra operación. Sin embargo, dado que sabemos de antemano qué bits nos interesan, podemos revisar nuestro filtro para asegurarnos de que no experimentamos colisiones de bits portadores:

Compare (-1 AND mask) * shift con el resultado esperado de todos los uno, -1 << (64-n) (para 64 bits sin signo)

El cambio mágico/multiplicar para extraer nuestros bits funciona si y solo si los dos son iguales.

 29
Author: Ternary,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-01-28 01:30:01

Además de las ya excelentes respuestas a esta pregunta muy interesante, podría ser útil saber que este truco de multiplicación bitwise ha sido conocido en la comunidad de ajedrez por computadora desde 2007, donde va bajo el nombre de BitBoards mágicos.

Muchos motores de ajedrez de computadora usan varios enteros de 64 bits (llamados bitboards) para representar los diversos conjuntos de piezas (1 bit por cuadrado ocupado). Supongamos que una pieza deslizante (torre, alfil, reina) en un cierto el cuadrado de origen puede moverse a como máximo K cuadrados si no hay piezas de bloqueo presentes. Usando bitwise-y de esos bits dispersos K con el bitboard de cuadrados ocupados da una palabra específica K-bit incrustada dentro de un entero de 64 bits.

La multiplicación mágica se puede usar para mapear estos bits K dispersos a los bits K inferiores de un entero de 64 bits. Estos bits K inferiores se pueden usar para indexar una tabla de bitboards pre-calculados que representan los cuadrados permitidos en los que se encuentra la pieza su cuadrado de origen en realidad se puede mover a (teniendo cuidado de bloquear piezas, etc.)

Un motor de ajedrez típico que usa este enfoque tiene 2 tablas (una para torres, una para alfiles, reinas usando la combinación de ambas) de 64 entradas (una por cuadrado de origen) que contienen tales resultados pre-calculados. Tanto la fuente cerrada mejor valorada (Houdini) y motor de ajedrez de código abierto(Stockfish) actualmente utilizar este enfoque para su muy alta rendimiento.

Encontrar estos multiplicadores mágicos se hace ya sea utilizando un búsqueda exhaustiva (optimizada con cortes tempranos) o con trial y erorr (por ejemplo, probar muchos enteros aleatorios de 64 bits). No se han utilizado patrones de bits durante la generación de movimientos para los cuales no se pudo encontrar ninguna constante mágica. Sin embargo, los efectos de transporte de bits son típicamente necesarios cuando los bits a asignar tienen índices (casi) adyacentes.

AFAIK, el muy general SAT-solver approachy by @ Syzygy no ha sido utilizado en ajedrez por computadora, y tampoco parece haber ninguna teoría formal con respecto a la existencia y singularidad de tales constantes mágicas.

 12
Author: TemplateRex,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-04-14 12:00:35