Extraer bits con una sola multiplicación

301

Vi una técnica interesante utilizada en una respuesta a 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 es necesario preservarlos.

La solución fue 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 usar esta técnica para extraer algún subconjunto de bits? Si no es así, ¿cómo se puede determinar si el método funciona o no para un conjunto particular de bits?

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

NPE
fuente
29
Si le pareció interesante, eche un vistazo a esta lista: graphics.stanford.edu/~seander/bithacks.html Muchos de ellos (ab) usan una multiplicación / división de enteros más amplios para lograr resultados interesantes. (La parte "Invertir los bits en un byte con 4 operaciones" muestra cómo lidiar con el truco de desplazamiento de bits / multiplicación cuando no tiene suficiente espacio y necesita enmascarar / multiplicar dos veces)
viraptor
@viraptor: excelente punto. Si comprende las limitaciones de este método, realmente puede usar la multiplicación para lograr mucho con respecto a la manipulación de bits.
Expedito
99
Curiosamente, hay una instrucción en AVX2 (que lamentablemente aún no está disponible) que realiza exactamente la operación que usted describe: software.intel.com/sites/products/documentation/studio/composer/…
JPvdMerwe
3
Otro lugar para buscar algoritmos inteligentes de
giro de
1
Um livro que conheço sobre o assunto (e gosto bastante) é o enlace
Salles

Respuestas:

235

Pregunta muy interesante y truco inteligente.

Veamos un ejemplo simple de cómo manipular un solo byte. Uso de 8 bits sin signo para simplificar. Imagina que tu número es xxaxxbxxy quieres ab000000.

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

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

Una multiplicación es un montón de operaciones shift-and-add. La clave es permitir que el desbordamiento "aleje" los bits que no necesitamos y coloque los que queremos en el lugar correcto.

La multiplicación por 4 ( 00000100) desplazaría todo a la izquierda por 2 y lo llevaría a a00b0000. Para bsubir, necesitamos multiplicar por 1 (para mantener la a en el lugar correcto) + 4 (para mover la b hacia arriba). Esta suma es 5, y combinada con las 4 anteriores obtenemos un número mágico de 20, o 00010100. El original fue 00a00b00después del enmascaramiento; la multiplicación da:

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

Desde este enfoque puede extenderse 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 permita varias operaciones de enmascaramiento o varias multiplicaciones. El problema es la cuestión de las "colisiones", por ejemplo, la "calle b" en el problema anterior. Imagina que necesitamos hacer esto a un número como xaxxbxxcx. Siguiendo el enfoque anterior, pensaría que necesitamos {x 2, x {1 + 4 + 16}} = x 42 (oooh, ¡la respuesta a todo!). Resultado:

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

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

Entonces, sin una prueba formal, respondería las partes más interesantes de su pregunta de la siguiente manera: "No, esto no funcionará para ningún número de bits. Para extraer N bits, necesita espacios (N-1) 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 adyacentes entre sí en el original, y desea mantenerlos en el mismo orden, entonces aún puedes hacerlo. Y para el propósito de la regla (N-1) cuentan como dos bits.

Hay otra idea, inspirada en la respuesta de @Ternary a continuación (vea mi comentario allí). Para cada bit interesante, solo necesita tantos ceros a la derecha como espacio para los bits que deben ir allí. Pero también, necesita tantos bits a la izquierda como bits de resultado a la izquierda. Entonces, si un bit b termina en la posición m de n, entonces necesita tener m-1 ceros a su izquierda y nm ceros a su derecha. Especialmente cuando los bits no están en el mismo orden en el número original que lo estarán después del reordenamiento, esta 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 cambiar a

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

aunque solo haya un espacio entre e y b, dos entre d y c, tres entre los demás. ¿Qué pasó con N-1? En este caso, se a...econvierte en "un bloque": se multiplican por 1 para terminar en el lugar correcto y, por lo tanto, "obtuvimos e gratis". Lo mismo es cierto para byd (b necesita tres espacios a la derecha, d necesita los mismos tres a su izquierda). Entonces, 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 (N-1)regla: "Siempre funcionará si hay al menos (N-1) espacios entre bits; o, si se conoce el orden de los bits en el resultado final, si un bit b termina en la posición m de n, necesita tener m-1 ceros a su izquierda y nm ceros a su derecha ".

@Ternary señaló que esta regla no funciona del todo, ya que puede haber un arrastre de bits agregando "justo a la derecha del área objetivo", es decir, cuando los bits que estamos buscando son todos. 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..

Por simplicidad, nombraré las posiciones de bit ABCDEFGHIJKLMNOP

Las matemáticas que íbamos a hacer era

ABCDEFGHIJKLMNOP

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

Hasta ahora, pensamos que nada por debajo de abcde(posiciones ABCDE) no importaría, pero de hecho, como se señaló @Ternary, si b=1, c=1, d=1a continuación, (b+c)en la posición Gcausará un poco para llevar a la posición F, lo que significa que (d+1)en la posición Fllevará un poco en E- y nuestra El resultado se echa a perder. Tenga en cuenta que el espacio a la derecha del bit de interés menos significativo ( cen este ejemplo) no importa, ya que la multiplicación causará relleno con ceros de beyone el bit menos significativo.

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

Tenemos que mirar no solo el número de bits que cumplen con el criterio (nm), sino también los que están en (n-m + 1), etc. Llamemos a su número Q0 (exactamente n-mal 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 observa esto, puede ver que si escribe una expresión matemática simple

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

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

Creo que seguir lo anterior te ayudará mucho a responder ...

Floris
fuente
1
Excelente. Otro problema sutil: la prueba m-1 / nm falla algunas veces debido a los bits de acarreo. Prueba a ... b..c ... d - terminas con b + c en el quinto bit, que si ambos son 1 hace un bit de acarreo que golpea d (!)
Ternario
1
resultado: n-1 bits de espacio prohíbe las configuraciones que deberían funcionar (es decir ... b..c ... d), y m-1 / nm permite las que no funcionan (a ... b..c ...re). No he podido encontrar una manera simple de caracterizar cuál funcionará y cuál no.
Ternario
¡Estas bien! El problema de acarreo significa que necesitamos un poco más de espacio a la derecha de cada bit como "protección". A primera vista, si hay al menos dos bits que tienen exactamente el mínimo nm a la derecha, debe aumentar el espacio en 1. Más generalmente, si hay P tales bits, necesita log2 (P) bits adicionales para derecho de cualquiera que tuviera el mínimo (mn). ¿Te parece bien?
Floris
Bueno, ese último comentario fue demasiado simplista. Creo que mi respuesta editada más recientemente muestra que log2 (P) no es el enfoque correcto. La propia respuesta de @ Ternary (a continuación) muestra de manera elegante cómo puede distinguir una combinación de bits en particular si no tiene una solución garantizada; creo que el trabajo anterior explica un poco más sobre eso.
Floris
1
Probablemente sea una coincidencia, pero esta respuesta fue aceptada cuando el número de votos a favor llegó a 127. Si has leído hasta aquí, sonreirás conmigo ...
Floris
154

Pregunta muy interesante de hecho. Estoy hablando con mis dos centavos, que es que, si logras establecer problemas como este en términos de lógica de primer orden sobre la teoría de vectores de bits, entonces los demostradores de teoremas son tus amigos y potencialmente pueden brindarte muy rápidamente. Respuestas a sus preguntas. Volvamos a plantear el problema como teorema:

"Existen algunas constantes de 64 bits 'máscara' y 'multiplicando' tales que, para todos los vectores de bits de 64 bits x, en la expresión y = (x & máscara) * 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 'máscara' y 'multiplicando' satisfacen esta propiedad. Entonces, expresemos esto en términos de algo que un probador de teoremas pueda entender, a saber, la entrada SMT-LIB 2:

(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 preguntemos al probador de teoremas Z3 si este 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! Reproduce el resultado dado en la publicación 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 de investigación naciente sobre la cual se han publicado pocos artículos. Una búsqueda de "program synthesis" filetype:pdfdebería comenzar.

Syzygy
fuente
2
¡Estoy impresionado! No sabía que la "lógica de primer orden sobre la teoría del vector de bits" era incluso un tema real que la gente estudiaba, y mucho menos que podría dar resultados tan interesantes. Muchas gracias por compartir esto.
Floris
@ AndrewBacker: ¿Alguien podría aclararme sobre qué punto hay en esta cosa llamada "SO-as-a-a-job"? Quiero decir, no paga nada. No puedes vivir solo de TAN representantes. Quizás te pueda dar algunos puntos en las entrevistas. Tal vez. Si el lugar de trabajo es lo suficientemente bueno como para reconocer el valor de la representación SO, y eso no es un hecho ...
Reincorporar a Monica el
3
Por supuesto. SO también es un juego (cualquier cosa con puntos lo es) para mucha gente. Solo la naturaleza humana, como cazar en / r / new para que puedas publicar el primer comentario y obtener karma. No tiene nada de malo, siempre y cuando las respuestas sigan siendo buenas. Estoy más feliz de poder votar el tiempo y el esfuerzo de alguien cuando es probable que realmente noten que alguien lo hizo. El aliento es algo bueno :) Y ... ese fue un comentario muy antiguo, y sigue siendo cierto. No veo cómo no está claro.
Andrew Backer
88

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

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

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

Esto solo funciona porque los bits que se deben recopilar no están demasiado juntos, por lo que la multiplicación de bits que no pertenecen juntos en nuestro esquema cae más allá de los 64 bits o en la parte inferior de 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 AND.

starblue
fuente
2
¡Gran explicación! Su breve respuesta hizo posible encontrar rápidamente el valor del "número mágico".
Expedito
44
Esta es realmente la mejor respuesta, pero no habría sido tan útil sin leer primero (la primera mitad) la respuesta de @ floris.
Andrew Backer
29

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

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

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

Daré algunos ejemplos para ilustrar:

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

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

...a.b....c...Falla porque bestá en los 2 bits posteriores a(y se coloca en el lugar de otra persona cuando cambiamos a):

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

...a...b.c...Falla porque bestá en los 2 bits anteriores c(y es empujado al lugar de otra persona cuando cambiamos c):

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

...a...bc...d... Funciona porque los bits consecutivos cambian juntos:

  a000bc000d
+ 0bc000d000
+ 000d000000
= abcd000000

Pero tenemos un problema. Si usamos en n-ilugar de n-1podríamos tener el siguiente escenario: ¿qué pasa si tenemos una colisión fuera de la parte que nos importa, algo que enmascararíamos al final, pero cuyos bits de transporte terminan interfiriendo en el importante rango sin máscara? ? (y nota: el n-1requisito asegura que esto no suceda asegurándose de que los i-1bits después de nuestro rango sin máscara estén claros cuando cambiamos el ibit th)

...a...b..c...d...Posible falla en los bits de acarreo, cestá n-1después b, pero cumple con los n-icriterios:

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

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

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

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

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

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

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

Ternario
fuente
Me gusta: tiene razón en que para cada bit, solo necesita tantos ceros a la derecha como espacio para los bits que deben ir allí. Pero también , necesita tantos bits a la izquierda como bits de resultado a la izquierda. Entonces, si un bit btermina en posición mde n, entonces necesita tener m-1ceros a su izquierda y n-m-1ceros a su derecha. Especialmente cuando los bits no están en el mismo orden en el número original que lo estarán después del reordenamiento, esta es una mejora importante para los criterios originales. Esto es divertido.
Floris
13

Además de las excelentes respuestas a esta pregunta tan interesante, podría ser útil saber que este truco de multiplicación bit a bit se conoce en la comunidad de ajedrez informático desde 2007, donde se conoce con el nombre de Magic BitBoards .

Muchos motores de ajedrez de computadora usan varios enteros de 64 bits (llamados tableros de bits) para representar los distintos conjuntos de piezas (1 bit por casilla ocupada). Suponga que una pieza deslizante (torre, alfil, reina) en un determinado cuadrado de origen puede moverse a la mayoría de los Kcuadrados si no había piezas de bloqueo. El uso de bit a bit y de esos Kbits dispersos con el bitboard de cuadrados ocupados proporciona una Kpalabra específica de bit incorporada dentro de un entero de 64 bits.

La multiplicación mágica se puede utilizar para asignar estos Kbits dispersos a los Kbits más bajos de un entero de 64 bits. Estos Kbits inferiores se pueden usar para indexar una tabla de tableros de bits precalculados que representan los cuadrados permitidos a los que la pieza en su cuadrado de origen puede moverse realmente (cuidando el bloqueo de piezas, etc.)

Un motor de ajedrez típico que usa este enfoque tiene 2 tablas (una para torres, una para obispos, reinas que usan la combinación de ambas) de 64 entradas (una por casilla de origen) que contienen dichos resultados precalculados. Tanto el motor de ajedrez de código abierto de más alta calificación ( Houdini ) como el de código abierto ( Stockfish ) utilizan actualmente este enfoque por su alto rendimiento.

La búsqueda de estos multiplicadores mágicos se realiza mediante una búsqueda exhaustiva (optimizada con puntos de corte iniciales) o con prueba y error (por ejemplo, probar muchos enteros aleatorios de 64 bits). No se han utilizado patrones de bits durante la generación de movimientos para los que no se pudo encontrar una constante mágica. Sin embargo, los efectos de acarreo a nivel de bit son típicamente necesarios cuando los bits a mapear tienen (casi) índices adyacentes.

AFAIK, el enfoque muy general de solución de SAT de @Syzygy no se ha utilizado en el ajedrez informático, y tampoco parece haber ninguna teoría formal sobre la existencia y la unicidad de tales constantes mágicas.

TemplateRex
fuente
Pensé que cualquiera que tenga antecedentes completos de CS formal habría saltado al enfoque SAT directamente al ver este problema. ¿Quizás la gente de CS encuentra el ajedrez sin interés? :(
Restablecer Monica
@KubaOber Es principalmente al revés: el ajedrez informático está dominado por bit-twiddlers que programan en C o ensamblado, y odian cualquier tipo de abstracción (C ++, plantillas, OO). Creo que eso asusta a los verdaderos chicos de CS :-)
TemplateRex