web-dev-qa-db-fra.com

Extraire des bits avec une seule multiplication

J'ai vu une technique intéressante utilisée dans un réponse à ne autre question , et j'aimerais la comprendre un peu mieux.

On nous donne un entier non signé de 64 bits, et nous nous intéressons aux bits suivants:

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

Plus précisément, nous aimerions les placer dans les huit premières places, comme suit:

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

Nous ne nous soucions pas de la valeur des bits indiqués par ., et ils ne doivent pas être préservés.

La solution consistait à masquer les bits non désirés et à multiplier le résultat par 0x2040810204081. Ceci, comme il s'avère, fait le tour.

À quel point cette méthode est-elle générale? Cette technique peut-elle être utilisée pour extraire un sous-ensemble de bits? Si non, comment déterminer si la méthode fonctionne ou non pour un ensemble de bits particulier?

Enfin, comment pourrait-on trouver le multiplicateur correct (a?) Pour extraire les bits donnés?

297
NPE

Question très intéressante et astuce intelligente.

Regardons un exemple simple de manipulation d'un octet. Utilisation de 8 bits non signés pour plus de simplicité. Imaginez que votre numéro est xxaxxbxx et que vous voulez ab000000.

La solution consistait en deux étapes: un bit masquage, suivi d'une multiplication. Le masque de bits est une opération AND simple qui convertit les bits sans intérêt en zéros. Dans le cas ci-dessus, votre masque serait 00100100 et le résultat 00a00b00.

Maintenant la partie difficile: transformer cela en ab.......

Une multiplication est un ensemble d'opérations de décalage et d'ajout. La clé est de permettre au débordement de "déplacer" les bits dont nous n’avons pas besoin et de mettre ceux que nous voulons au bon endroit.

Multiplication par 4 (00000100) déplacerait tout ce qui restait de 2 et vous amènerait à a00b0000. Pour que le b progresse, il faut multiplier par 1 (pour maintenir le a au bon endroit) + 4 (pour augmenter le b). Cette somme est 5 et, avec les 4 précédents, nous obtenons un nombre magique de 20, ou 00010100. L'original était 00a00b00 après le masquage; la multiplication donne:

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

De cette approche, vous pouvez étendre à des nombres plus grands et plus de bits.

Une des questions que vous avez posées était "cela peut-il être fait avec un nombre quelconque de bits?" Je pense que la réponse est "non", à moins que vous n'autorisiez plusieurs opérations de masquage ou plusieurs multiplications. Le problème est la question des "collisions" - par exemple, le "parasite b" dans le problème ci-dessus. Imaginons que nous devions faire cela avec un nombre tel que xaxxbxxcx. En suivant l'approche précédente, on pourrait penser que nous avons besoin de {x 2, x {1 + 4 + 16}} = x 42 (oooh - la réponse à tout!). Résultat:

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

Comme vous pouvez le constater, cela fonctionne toujours, mais "seulement". La clé ici est qu’il y a "assez d’espace" entre les bits que nous voulons pour pouvoir tout compresser. Je ne pouvais pas ajouter un quatrième bit d juste après c, car j'obtiendrais des instances où je recevais c + d, des bits pourraient être transportés, ...

Donc, sans preuve formelle, je répondrais comme suit aux parties les plus intéressantes de votre question: "Non, cela ne fonctionnera pour aucun nombre de bits. Pour extraire N bits, vous avez besoin de (N-1) espaces entre les bits que vous souhaitez extraire ou avoir des étapes supplémentaires masque-multiplier ".

La seule exception à laquelle je peux penser en ce qui concerne la règle "doit avoir (N-1) zéros entre bits" est la suivante: si vous voulez extraire deux bits adjacents dans l'original, ET vous voulez les conserver dans le même ordre, alors vous pouvez toujours le faire. Et aux fins de la règle (N-1), ils comptent pour deux bits.

Il y a une autre idée - inspirée par la réponse de @Ternary ci-dessous (voir mon commentaire ici). Pour chaque bit intéressant, vous n'avez besoin que d'autant de zéros que vous avez besoin d'espace pour les bits qui doivent aller là-bas. Mais aussi, il faut autant de bits à gauche que de résultats à gauche. Donc, si un bit b termine en position m sur n, il doit alors avoir m-1 zéros à sa gauche et n-m zéros à sa droite. En particulier lorsque les bits ne sont pas dans le même ordre que dans le numéro d'origine, comme ils le seront après la re-commande, il s'agit d'une amélioration importante par rapport aux critères d'origine. Cela signifie, par exemple, qu'un mot 16 bits

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

Peut être déplacé dans

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

même s'il n'y a qu'un seul espace entre e et b, deux entre d et c, trois entre les autres. Qu'est-il arrivé à N-1 ?? Dans ce cas, a...e devient "un bloc" - ils sont multipliés par 1 pour se retrouver au bon endroit, et "nous en avons donc gratuitement". La même chose est vraie pour b et d (b nécessite trois espaces à droite, d nécessite les mêmes trois à gauche). Ainsi, lorsque nous calculons le nombre magique, nous trouvons des doublons:

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

Clairement, si vous vouliez que ces nombres soient dans un ordre différent, vous devrez les espacer davantage. Nous pouvons reformuler le (N-1) règle: "Cela fonctionnera toujours s’il ya au moins (N-1) espaces entre les bits; ou, si l’ordre des bits dans le résultat final est connu, alors si un bit b se termine à la position m sur n, il doit avoir m-1 zéros à sa gauche et nm zéros à sa droite. "

@Ternary a souligné que cette règle ne fonctionne pas tout à fait, car il peut y avoir un report de bits ajoutant "juste à la droite de la zone cible" - à savoir, lorsque les bits que nous recherchons le sont tous. En reprenant l’exemple que j’ai donné ci-dessus avec les cinq bits très serrés dans un mot de 16 bits: si on commence

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

Pour plus de simplicité, je nommerai les positions de bits ABCDEFGHIJKLMNOP

Le calcul que nous allions faire était

ABCDEFGHIJKLMNOP

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

Jusqu'à présent, nous pensions que tout ce qui se trouvait en dessous de abcde (les positions ABCDE) n'aurait pas d'importance, mais en réalité, comme l'a souligné @Ternary, si b=1, c=1, d=1 puis (b+c) en position G fera en sorte qu'un bit soit reporté en position F, ce qui signifie que (d+1) en position F portera un peu dans E - et notre résultat est gâché. Notez que l’espace à droite du bit d’intérêt le moins significatif (c dans cet exemple) n’a pas d’importance, car la multiplication provoquera le remplissage avec des zéros au-delà du bit le moins significatif.

Nous devons donc modifier notre règle (m-1)/(n-m). S'il y a plus d'un bit qui a "exactement (nm) bits inutilisés à droite (sans compter le dernier bit du motif -" c "dans l'exemple ci-dessus), nous devons renforcer la règle - et nous devons faites-le de manière itérative!

Nous devons non seulement regarder le nombre de bits qui répondent au critère (n-m), mais aussi ceux qui sont en (n-m + 1), etc. Appelons leur numéro Q0 (exactement n-m au bit suivant), Q1 (n-m + 1), jusqu’à Q(N-1) (n-1). Nous risquons alors de reporter si

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

Si vous regardez cela, vous pouvez voir que si vous écrivez une expression mathématique simple

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

et le résultat est W > 2 * N, vous devez alors augmenter le critère RHS d’un bit à (n-m+1). À ce stade, l’opération est sécurisée tant que W < 4; si cela ne fonctionne pas, augmentez le critère un de plus, etc.

Je pense que suivre ce qui précède vous amènera un long chemin à votre réponse ...

232
Floris

Question très intéressante en effet. J'interviens avec mes deux sous, à savoir que si vous parvenez à énoncer des problèmes de ce type en termes de logique de premier ordre par rapport à la théorie des vecteurs de bits, les prouveurs de théorèmes sont vos amis et peuvent vous fournir très rapidement réponses à vos questions. Reprenons le problème posé sous forme de théorème:

"Il existe des constantes 'masque' et 'multiplicande' de 64 bits telles que, pour tous les vecteurs de bits 64 bits x, dans l'expression y = (x & masque) * multiplicande, nous avons y.63 == x.63 , y.62 == x.55, y.61 == x.47, etc. "

Si cette phrase est en fait un théorème, il est vrai que certaines valeurs des constantes "masque" et "multiplicande" vérifient cette propriété. Formulons donc ceci en termes de quelque chose qu'un prouveur de théorèmes peut comprendre, à savoir l'entrée 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)

Et maintenant, demandons au prouveur de théorème Z3 s'il s'agit d'un théorème:

z3.exe /m /smt2 ExtractBitsThroughAndWithMultiplication.smt2

Le résultat est:

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

Bingo! Il reproduit le résultat donné dans le message d'origine en 0.06 secondes.

Considéré sous un angle plus général, il peut s’agir d’un exemple de problème de synthèse de programme de premier ordre, qui constitue un domaine de recherche naissant sur lequel peu d’articles ont été publiés. Une recherche de "program synthesis" filetype:pdf devrait vous aider à démarrer.

152
Syzygy

Chaque bit du multiplicateur est utilisé pour copier l’un des bits dans sa position correcte:

  • 1 est déjà dans la bonne position, multipliez donc par 0x0000000000000001.
  • 2 doit être décalé de 7 bits vers la gauche, nous multiplions donc par 0x0000000000000080 _ (le bit 7 est activé).
  • 3 doit être décalé de 14 bits vers la gauche, nous multiplions donc par 0x0000000000000400 _ (le bit 14 est activé).
  • et ainsi de suite jusqu'à
  • 8 doit être décalé de 49 bits vers la gauche, nous multiplions donc par 0x0002000000000000 _ (le bit 49 est activé).

Le multiplicateur est la somme des multiplicateurs pour les bits individuels.

Cela ne fonctionne que parce que les bits à collecter ne sont pas trop proches les uns des autres, de sorte que la multiplication des bits qui n'appartiennent pas ensemble dans notre schéma tombe au-delà du 64 bits ou dans la partie sans importance.

Notez que les autres bits du nombre d'origine doivent être 0. Ceci peut être réalisé en les masquant avec une opération AND.

88
starblue

(Je ne l'avais jamais vu auparavant. Cette astuce est géniale!)

J'expliquerai un peu plus sur l'affirmation de Floris selon laquelle, lors de l'extraction de bits n, vous avez besoin de n-1 D'espace entre tous les bits non consécutifs:

Ma pensée initiale (nous verrons dans une minute comment cela ne fonctionne pas tout à fait) était que vous pouviez faire mieux: si vous voulez extraire des bits n, vous aurez une collision lors de l'extraction/du décalage d'un bit i si vous avez quelqu'un (non consécutif avec le bit i) dans les bits i-1 Précédents ou n-i Suivants.

Je vais donner quelques exemples pour illustrer:

...a..b...c... Works (personne dans les 2 bits après a, le bit avant et le bit après b, et personne ne se trouve dans les 2 bits avant c) :

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

...a.b....c... Échoue car b est dans les 2 bits après a (et est attiré dans la position de quelqu'un d'autre lorsque nous passons a):

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

...a...b.c... Échoue car b est dans les 2 bits précédant c (et est poussé à la place de quelqu'un d'autre lorsque nous passons c):

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

...a...bc...d... Fonctionne parce que des bits consécutifs sont transférés ensemble:

  a000bc000d
+ 0bc000d000
+ 000d000000
= abcd000000

Mais nous avons un problème. Si nous utilisons n-i Au lieu de n-1, Nous pourrions avoir le scénario suivant: et si nous avions une collision en dehors de la partie qui nous nous soucions de quelque chose que nous masquerions à la fin, mais dont les bits de retenue finissent par s’immiscer dans l’importante plage non masquée? (et remarque: l'exigence n-1 garantit que cela ne se produise pas en s'assurant que les bits i-1 situés après la plage non masquée sont clairs lorsque nous passons le i bit)

...a...b..c...d... Échec potentiel sur les carry-bits, c est dans n-1 Après b, mais satisfait aux critères n-i:

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

Alors pourquoi ne pas simplement revenir à cette exigence "n-1 Bits d'espace"? Parce que nous pouvons faire mieux:

...a....b..c...d.. échoue le test "n-1 Bits d'espace", mais fonctionne pour notre astuce d'extraction de bits:

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

Je ne peux pas trouver un bon moyen de caractériser ces champs qui n'ont pas un espace de n-1 Entre des bits importants, mais toujours fonctionnerait pour notre opération. Cependant, puisque nous savons à l'avance quels bits nous intéressons, nous pouvons vérifier notre filtre pour nous assurer que nous n'éprouvons pas de collisions de retenue:

Comparez (-1 AND mask) * shift Au résultat global attendu, -1 << (64-n) (pour les signatures 64 bits non signées)

Le décalage/multiplication magique pour extraire nos bits fonctionne si et seulement si les deux sont égaux.

29
Ternary

En plus des réponses déjà excellentes à cette question très intéressante, il peut être utile de savoir que cette astuce de multiplication au niveau du bit est connue de la communauté des échecs informatiques depuis 2007, où elle se nomme Magic BitBoards .

De nombreux moteurs d’échecs informatiques utilisent plusieurs entiers 64 bits (appelés bitboards) pour représenter les différents ensembles de pièces (1 bit par carré occupé). Supposons qu'une pièce coulissante (tour, évêque, reine) sur un certain carré d'origine puisse se déplacer sur au plus K carrés s'il n'y a pas de bloc bloquant. L'utilisation de bits dispersés K avec le bitboard de carrés occupés donne un mot spécifique K - incorporé dans un entier de 64 bits.

La multiplication magique peut être utilisée pour mapper ces bits dispersés K aux bits inférieurs K d'un entier de 64 bits. Ces bits inférieurs K peuvent ensuite être utilisés pour indexer une table de bitboards pré-calculés qui représente les carrés autorisés vers lesquels la pièce sur son carré d'origine peut se déplacer (en prenant soin de bloquer des pièces, etc.).

Un moteur d'échecs typique utilisant cette approche a 2 tables (une pour les tours, une pour les évêques, les reines utilisant la combinaison des deux) de 64 entrées (une par carré d'origine) contenant de tels résultats pré-calculés. La source fermée la mieux notée ( Houdini ) et le moteur d’échecs open source ( Stockfish ) utilise actuellement cette approche pour ses performances très élevées.

La recherche de ces multiplicateurs magiques se fait soit à l'aide d'une recherche exhaustive (optimisée avec les premiers seuils) ou avec essai et erorr (par exemple, essayer beaucoup d'entiers aléatoires de 64 bits). Il n’ya eu aucun motif de bits utilisé pendant la génération de mouvements pour lequel aucune constante magique n’a pu être trouvée. Cependant, des effets de transfert au niveau du bit sont généralement nécessaires lorsque les bits à mapper ont des index (presque) adjacents.

AFAIK, l'approche très générale de résolution de SAT par @Syzygy n'a pas été utilisée dans les échecs sur ordinateur, et il ne semble pas non plus exister de théorie formelle concernant l'existence et le caractère unique de telles constantes magiques.

12
TemplateRex