Extraire des bits avec une seule multiplication


301

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

On nous donne un entier 64 bits non signé, et nous sommes intéressés par les bits suivants:

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

Plus précisément, nous aimerions les déplacer vers les huit premières positions, comme ceci:

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

Nous ne nous soucions pas de la valeur des bits indiqués par ., et ils n'ont pas besoin d'être conservés.

La solution était de masquer les bits indésirables et de multiplier le résultat par 0x2040810204081. Il s'avère que cela fait l'affaire.

Quelle est la généralité de cette méthode? Cette technique peut-elle être utilisée pour extraire n'importe quel sous-ensemble de bits? Sinon, comment déterminer si la méthode fonctionne ou non pour un ensemble particulier de bits?

Enfin, comment procéder pour trouver le multiplicateur (a?) Correct pour extraire les bits donnés?


29
Si vous avez trouvé celui-là intéressant, consultez cette liste: graphics.stanford.edu/~seander/bithacks.html Beaucoup d'entre eux (ab) utilisent une multiplication / division plus large pour obtenir des résultats intéressants. (La partie "Inverser les bits dans un octet avec 4 opérations" montre comment gérer l'astuce de décalage / multiplication lorsque vous n'avez pas assez d'espace et que vous devez masquer / multiplier deux fois)
viraptor

@viraptor: Excellent point. Si vous comprenez les limites de cette méthode, vous pouvez vraiment utiliser la multiplication pour accomplir beaucoup de choses en ce qui concerne la manipulation des bits.
Expedito

9
Il est donc intéressant de noter qu'il existe une instruction dans AVX2 (qui n'est malheureusement pas encore disponible) qui effectue exactement l'opération que vous décrivez: software.intel.com/sites/products/documentation/studio/composer/…
JPvdMerwe

3
Un autre endroit où chercher des algorithmes de
twiddling

1
Um livro que conheço sobre o assunto (e gosto bastante) é o "Hacker's Delight" link
Salles

Réponses:


235

Question très intéressante et astuce astucieuse.

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

La solution consistait en deux étapes: un peu de masquage, suivi d'une multiplication. Le masque de bits est une opération ET simple qui transforme les bits non intéressants en zéros. Dans le cas ci-dessus, votre masque serait 00100100et 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.

La multiplication par 4 ( 00000100) déplacerait tout ce qui reste par 2 et vous amènerait à a00b0000. Pour faire bmonter le nous devons multiplier par 1 (pour garder le a au bon endroit) + 4 (pour déplacer le b vers le haut). Cette somme est 5, et combinée avec les 4 précédentes, nous obtenons un nombre magique de 20, ou 00010100. L'original était 00a00b00après le masquage; la multiplication donne:

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

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

L'une des questions que vous avez posées était "est-ce que cela peut être fait avec n'importe quel nombre de bits?" Je pense que la réponse est "non", sauf si vous autorisez plusieurs opérations de masquage ou plusieurs multiplications. Le problème est la question des "collisions" - par exemple, le "b errant" dans le problème ci-dessus. Imaginez que nous devons faire cela à un nombre comme 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 voir, cela fonctionne toujours, mais "seulement". La clé ici est qu'il y a "suffisamment d'espace" entre les bits que nous voulons pour pouvoir tout serrer. Je ne pouvais pas ajouter un quatrième bit d juste après c, car j'obtiendrais des instances où j'obtiens c + d, des bits pourraient porter, ...

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

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

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

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

Peut être déplacé en

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...edevient "un bloc" - ils sont multipliés par 1 pour se retrouver au bon endroit, et donc "nous avons eu e gratuitement". Il en va de même pour b et d (b a besoin de trois espaces à droite, d a besoin des mêmes trois à gauche). Ainsi, lorsque nous calculons le nombre magique, nous constatons qu'il y a des doublons:

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

De toute évidence, si vous vouliez ces chiffres dans un ordre différent, vous devriez les espacer davantage. Nous pouvons reformuler la (N-1)règle: "Cela fonctionnera toujours s'il y a 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 retrouve en position m de 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 à droite de la zone cible" - à savoir, lorsque les bits que nous recherchons sont tous un. Poursuivant l'exemple que j'ai donné ci-dessus avec les cinq bits serrés dans un mot de 16 bits: si nous commençons par

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

Pour simplifier, je nommerai les positions des 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 était en dessous abcde(positions ABCDE) n'aurait pas d'importance, mais en fait, comme l'a souligné @Ternary, si b=1, c=1, d=1alors (b+c)en position G, un peu se poursuivra en position F, ce qui signifie qu'en (d+1)position, Fcela portera un peu dans E- et notre le résultat est gâté. 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 un remplissage avec des zéros de au-delà du bit le moins significatif.

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

Nous devons regarder non seulement le nombre de bits qui répondent au critère (nm), mais aussi ceux qui sont à (n-m + 1), etc. Appelons leur nombre Q0 (exactement n-mau bit suivant), Q1 ( n-m + 1), jusqu'à Q (N-1) (n-1). Ensuite, nous risquons de porter 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, alors vous devez augmenter le critère RHS d'un bit à (n-m+1). À ce stade, l'opération est sûre tant que W < 4; si cela ne fonctionne pas, augmentez encore le critère, etc.

Je pense qu'en suivant ce qui précède, vous obtiendrez un long chemin vers votre réponse ...


1
Génial. Un problème plus subtil: le test m-1 / nm échoue une partie du temps en raison de bits de portage. Essayez a ... b..c ... d - vous vous retrouvez avec b + c dans le cinquième bit qui, s'ils sont tous les deux, 1 fait un peu de retenue qui saisit d (!)
Ternary

1
résultat: n-1 bits d'espace interdit les configurations qui devraient fonctionner (iea ... b..c ... d), et m-1 / nm permet celles qui ne fonctionnent pas (a ... b..c ...ré). Je n'ai pas été en mesure de trouver un moyen simple de caractériser ce qui fonctionnera et ce qui ne fonctionnera pas.
Ternary

Vous êtes doué! Le problème de portage signifie que nous avons besoin d'un peu plus d'espace à droite de chaque bit comme "protection". À première vue, s'il y a au moins deux bits qui ont exactement le nm minimum à droite, vous devez augmenter l'espace de 1. Plus généralement, s'il y a P de tels bits, vous avez besoin de log2 (P) bits supplémentaires à la droit de celui qui avait le minimum (mn). Cela vous semble juste?
Floris

Eh bien, ce dernier commentaire était trop simpliste. Je pense que ma dernière réponse éditée montre que log2 (P) n'est pas la bonne approche. @ La propre réponse de Ternary (ci-dessous) montre avec élégance comment vous pouvez déterminer une combinaison de bits particulière si vous n'avez pas de solution garantie - je pense que le travail ci-dessus en développe un peu plus.
Floris

1
C'est probablement une coïncidence, mais cette réponse a été acceptée lorsque le nombre de votes positifs a atteint 127. Si vous avez lu jusqu'ici, vous sourirez avec moi ...
Floris

154

Question très intéressante en effet. J'interviens avec mes deux cents, ce qui est que, si vous parvenez à énoncer des problèmes comme celui-ci en termes de logique de premier ordre sur la théorie des vecteurs de bits, alors les prouveurs de théorèmes sont votre ami, et peuvent potentiellement vous fournir très rapidement réponses à vos questions. Reprenons le problème posé comme théorème:

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

Si cette phrase est en fait un théorème, alors il est vrai que certaines valeurs des constantes «masque» et «multiplicande» satisfont cette propriété. Exprimons donc ceci en termes de quelque chose qu'un prouveur de théorème 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 seconde.

En examinant cela d'un point de vue plus général, nous pouvons voir cela comme un exemple d'un problème de synthèse de programme de premier ordre, qui est un domaine de recherche naissant sur lequel peu d'articles ont été publiés. Une recherche de "program synthesis" filetype:pdfdevrait vous aider à démarrer.


2
Je suis impressionné! Je ne savais pas que "la logique du premier ordre sur la théorie des vecteurs de bits" était même un vrai sujet que les gens étudiaient - encore moins qu'il pouvait donner des résultats si intéressants. Merci beaucoup d'avoir partagé cela.
Floris

@AndrewBacker: Quelqu'un pourrait-il m'éclairer sur le point qu'il y a dans cette soi-disant chose "SO-as-a-job"? Je veux dire, ça ne paie rien. Vous ne pouvez pas vivre avec SO seul. Peut-être que cela peut vous donner quelques points dans les interviews. Peut être. Si le lieu de travail est assez bon pour reconnaître la valeur du représentant SO, et ce n'est pas une évidence ...
Rétablir Monica le

3
Sûr. SO est aussi un jeu (n'importe quoi avec des points l'est) pour beaucoup de gens. Juste la nature humaine, comme la chasse dans / r / new pour que vous puissiez poster le premier commentaire et obtenir le karma. Rien de mal à cela, tant que les réponses sont toujours bonnes. Je suis juste plus heureux de pouvoir surestimer le temps et les efforts de quelqu'un alors qu'il est susceptible de le remarquer. L'encouragement est une bonne chose :) Et ... c'était un très vieux commentaire, et toujours vrai. Je ne vois pas comment ce n'est pas clair.
Andrew Backer

88

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

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

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 inférieure ne se soucie pas.

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


2
Grande explication! Votre réponse courte a permis de trouver rapidement la valeur du «nombre magique».
Expedito

4
C'est vraiment la meilleure réponse, mais cela n'aurait pas été aussi utile sans lire (la première moitié) de la réponse de @ floris en premier.
Andrew Backer

29

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

Je développerai un peu l'assertion de Floris selon laquelle lors de l'extraction de nbits, vous avez besoin d' n-1espace entre les bits non consécutifs:

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

Je vais donner quelques exemples pour illustrer:

...a..b...c...Fonctionne (personne dans les 2 bits après a, le bit avant et le bit après b, et personne n'est dans les 2 bits avant c):

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

...a.b....c...Échoue car se btrouve dans les 2 bits après a(et se fait tirer à la place de quelqu'un d'autre lorsque nous passons a):

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

...a...b.c...Échoue car se btrouve dans les 2 bits précédents 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 les bits consécutifs se déplacent ensemble:

  a000bc000d
+ 0bc000d000
+ 000d000000
= abcd000000

Mais nous avons un problème. Si nous utilisons à la n-iplace de n-1nous pourrions avoir le scénario suivant: que se passe-t-il si nous avons une collision en dehors de la partie qui nous intéresse, quelque chose que nous masquons à la fin, mais dont les bits de transport finissent par interférer dans la plage non masquée importante ? (et remarque: l' n-1exigence fait en sorte que cela ne se produise pas en s'assurant que les i-1bits après notre plage non masquée sont clairs lorsque nous décalons le ith bit)

...a...b..c...d...La défaillance potentielle sur les bits de portage se csitue n-1après b, mais satisfait aux n-icritères:

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

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

...a....b..c...d.. Échoue le n-1test " 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 d' n-1espace entre les bits importants, mais qui fonctionneraient quand même pour notre opération. Cependant, puisque nous savons à l'avance quels bits nous intéressent, nous pouvons vérifier notre filtre pour nous assurer que nous ne subissons pas de collisions de bits de transport:

Comparer (-1 AND mask) * shiftavec le résultat tout-en-un attendu,-1 << (64-n) (pour 64 bits non signé)

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


J'aime ça - vous avez raison de dire que pour chaque bit, vous n'avez besoin que de zéros à droite de celui-ci car vous avez besoin d'espace pour les bits qui doivent y aller. Mais aussi , il a besoin d'autant de bits à gauche qu'il y a de bits de résultat à gauche. Donc, si un bit bse retrouve en position mde n, il doit avoir des m-1zéros à sa gauche et des n-m-1zéros à sa droite. Surtout lorsque les bits ne sont pas dans le même ordre dans le numéro d'origine qu'ils le seront après la réorganisation, il s'agit d'une amélioration importante par rapport aux critères d'origine. C'est marrant!
Floris

13

En plus des réponses déjà excellentes à cette question très intéressante, il pourrait être utile de savoir que cette astuce de multiplication au niveau du bit est connue dans la communauté des échecs informatiques depuis 2007, où elle s'appelle 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 une certaine case d'origine puisse se déplacer sur la plupart des Kcases si aucune pièce de blocage n'était présente. L'utilisation au niveau du bit et de ces Kbits dispersés avec le bitboard des carrés occupés donne unK mot bits incorporé dans un entier de 64 bits.

La multiplication magique peut être utilisée pour mapper ces Kbits dispersés aux bits inférieurs Kd'un entier 64 bits. Ces plus basK bits peuvent ensuite être utilisés pour indexer un tableau de tableaux de bord pré-calculés qui représentent les carrés autorisés vers lesquels la pièce sur son carré d'origine peut réellement se déplacer (en prenant soin de bloquer les 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) qui contiennent de tels résultats pré-calculés. La source fermée la mieux notée ( Houdini ) et le moteur d'échecs open source ( Stockfish ) utilisent actuellement cette approche pour ses très hautes performances.

La recherche de ces multiplicateurs magiques se fait soit en utilisant une recherche exhaustive (optimisée avec des coupures précoces), soit avec des essais et des erreurs. (par exemple en essayant de nombreux nombres entiers aléatoires de 64 bits). Il n'y a eu aucun motif de bits utilisé pendant la génération de mouvement pour lequel aucune constante magique n'a pu être trouvée. Cependant, des effets de transport au niveau du bit sont généralement nécessaires lorsque les bits à mapper ont des indices (presque) adjacents.

AFAIK, l'approche très générale du solveur SAT par @Syzygy n'a pas été utilisée dans les échecs informatiques, et il ne semble pas non plus y avoir de théorie formelle concernant l'existence et l'unicité de ces constantes magiques.


J'aurais pensé que quiconque ayant une expérience formelle complète de CS aurait sauté à l'approche SAT directement en voyant ce problème. Peut-être que les gens CS trouvent les échecs sans intérêt? :(
Rétablir Monica

@KubaOber C'est surtout l'inverse: les échecs informatiques sont dominés par les bit-twiddlers qui programment en C ou en assembleur, et détestent tout type d'abstraction (C ++, templates, OO). Je pense que cela fait peur aux vrais gars CS :-)
TemplateRex
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.