Estrarre bit con una singola moltiplicazione


301

Ho visto una tecnica interessante utilizzata in una risposta a un'altra domanda e vorrei capirla un po 'meglio.

Ci viene dato un numero intero a 64 bit senza segno e siamo interessati ai seguenti bit:

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

In particolare, vorremmo spostarli nelle prime otto posizioni, in questo modo:

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

Non ci interessa il valore dei bit indicati da .e non devono essere preservati.

La soluzione era mascherare i bit indesiderati e moltiplicare il risultato per 0x2040810204081. Questo, a quanto pare, fa il trucco.

Quanto è generale questo metodo? Questa tecnica può essere utilizzata per estrarre qualsiasi sottoinsieme di bit? In caso contrario, come si può capire se il metodo funziona o meno per un determinato insieme di bit?

Infine, come si potrebbe trovare il moltiplicatore (a?) Corretto per estrarre i bit dati?


29
Se lo hai trovato interessante, dai un'occhiata a questo elenco: graphics.stanford.edu/~seander/bithacks.html Molti di loro (ab) usano una moltiplicazione / divisione di numeri più grandi per ottenere risultati interessanti. (La parte "Inverti i bit in un byte con 4 operazioni" mostra come gestire il trucco del bitshift / moltiplicazione quando non hai abbastanza spazio e devi mascherare / moltiplicare due volte)
viraptor

@viraptor: punto eccellente. Se capisci i limiti di questo metodo, puoi davvero usare la moltiplicazione per ottenere molto rispetto alla manipolazione dei bit.
Expedito,

9
È interessante notare che in AVX2 sono presenti istruzioni (che purtroppo non sono ancora disponibili) che
eseguono

3
Un altro posto per cercare gli algoritmi bit-giocherellando intelligente è MIT HAKMEM
Barmar

1
Um livro que conheço sobre o assunto (e gosto bastante) é o link
Salles

Risposte:


235

Domanda molto interessante e trucco intelligente.

Vediamo un semplice esempio di come manipolare un singolo byte. Utilizzo di unsigned 8 bit per semplicità. Immagina che il tuo numero sia xxaxxbxxe desideri ab000000.

La soluzione consisteva in due passaggi: un po 'di mascheramento, seguito da moltiplicazione. La maschera di bit è una semplice operazione AND che trasforma i bit non interessanti in zeri. Nel caso sopra, la maschera sarebbe 00100100e il risultato 00a00b00.

Ora la parte difficile: trasformarla in ab.......

Una moltiplicazione è un gruppo di operazioni di spostamento e aggiunta. La chiave è consentire all'overflow di "spostare" i bit di cui non abbiamo bisogno e mettere quelli che vogliamo nel posto giusto.

La moltiplicazione per 4 ( 00000100) sposta tutto ciò che rimane per 2 e ti porta a a00b0000. Per far bmuovere verso l'alto dobbiamo moltiplicare per 1 (per mantenere la a nel posto giusto) + 4 (per spostare la b in alto). Questa somma è 5, e combinata con i precedenti 4 otteniamo un numero magico di 20, o 00010100. L'originale era 00a00b00dopo il mascheramento; la moltiplicazione dà:

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

Da questo approccio è possibile estendere a numeri più grandi e più bit.

Una delle domande che hai posto è stata "può essere fatto con un numero qualsiasi di bit?" Penso che la risposta sia "no", a meno che non si consentano diverse operazioni di mascheramento o diverse moltiplicazioni. Il problema è il problema delle "collisioni", ad esempio la "randagia b" nel problema precedente. Immagina di dover fare questo per un numero simile xaxxbxxcx. Seguendo l'approccio precedente, potresti pensare che abbiamo bisogno di {x 2, x {1 + 4 + 16}} = x 42 (oooh - la risposta a tutto!). Risultato:

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

Come puoi vedere, funziona ancora, ma "solo". La chiave qui è che c'è "spazio sufficiente" tra i bit che vogliamo per poter spremere tutto. Non potrei aggiungere un quarto bit d subito dopo c, perché otterrei casi in cui ottengo c + d, i bit potrebbero portare, ...

Quindi, senza una prova formale, risponderei alle parti più interessanti della tua domanda come segue: "No, questo non funzionerà per nessun numero di bit. Per estrarre N bit, hai bisogno di (N-1) spazi tra i bit che vuoi estrarre o disporre di ulteriori passaggi di moltiplicazione maschera. "

L'unica eccezione che mi viene in mente per la regola "deve avere (N-1) zeri tra bit" è questa: se vuoi estrarre due bit adiacenti l'uno nell'originale E vuoi tenerli nel stesso ordine, quindi puoi ancora farlo. E ai fini della regola (N-1) contano come due bit.

C'è un'altra intuizione - ispirata alla risposta di @Ternary di seguito (vedi il mio commento lì). Per ogni bit interessante, hai solo bisogno di tanti zeri a destra di quanti ne hai bisogno di spazio per i bit che devono andare lì. Inoltre, ha bisogno di tanti bit a sinistra quanti bit di risultato a sinistra. Quindi se un bit b finisce nella posizione m di n, allora deve avere zeri m-1 alla sua sinistra e zeri nm alla sua destra. Soprattutto quando i bit non sono nello stesso ordine nel numero originale come saranno dopo il riordino, questo è un miglioramento importante dei criteri originali. Ciò significa, ad esempio, che una parola a 16 bit

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

Può essere spostato in

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

anche se c'è solo uno spazio tra eeb, due tra d e c, tre tra gli altri. Qualunque cosa sia successa a N-1 ?? In questo caso, a...ediventa "un blocco" - vengono moltiplicati per 1 per finire nel posto giusto, quindi "abbiamo ottenuto e gratuitamente". Lo stesso vale per be d (b ha bisogno di tre spazi a destra, d ha bisogno degli stessi tre a sinistra). Quindi, quando calcoliamo il numero magico, troviamo che ci sono duplicati:

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

Chiaramente, se volessi questi numeri in un ordine diverso, dovresti spaziarli ulteriormente. Possiamo riformulare il(N-1) regola: "Funzionerà sempre se ci sono almeno (N-1) spazi tra i bit; oppure, se si conosce l'ordine dei bit nel risultato finale, allora se un bit b finisce nella posizione m di n, deve avere zeri m-1 alla sua sinistra e zeri nm alla sua destra. "

@Ternary ha sottolineato che questa regola non funziona del tutto, poiché può esserci un riporto da bit che aggiungono "appena a destra dell'area target" - vale a dire, quando i bit che stiamo cercando sono tutti uno. Continuando l'esempio che ho dato sopra con i cinque bit ben confezionati in una parola di 16 bit: se iniziamo con

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

Per semplicità, nominerò le posizioni dei bit ABCDEFGHIJKLMNOP

La matematica che stavamo per fare era

ABCDEFGHIJKLMNOP

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

Fino ad ora, abbiamo pensato che qualcosa sotto abcde(posizioni ABCDE) non avrebbe avuto importanza, ma in effetti, come sottolineato da @Ternary, se b=1, c=1, d=1poi (b+c)in posizione Gcauserà un po 'di portare in posizione F, il che significa che (d+1)in posizione Fporterà un po' inE - e il nostro il risultato è rovinato. Si noti che lo spazio a destra del bit di interesse meno significativo ( cin questo esempio) non ha importanza, poiché la moltiplicazione causerà il riempimento con zeri di beyone il bit meno significativo.

Quindi dobbiamo modificare la nostra regola (m-1) / (nm). Se c'è più di un bit che ha "esattamente (nm) bit inutilizzati a destra (non contando l'ultimo bit nel modello -" c "nell'esempio sopra), allora dobbiamo rafforzare la regola - e dobbiamo fallo iterativamente!

Dobbiamo guardare non solo al numero di bit che soddisfano il criterio (nm), ma anche a quelli che si trovano in (n-m + 1), ecc. Chiamiamo il loro numero Q0 (esattamente n-mal bit successivo), Q1 ( n-m + 1), fino a Q (N-1) (n-1). Quindi rischiamo di trasportare if

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

Se lo guardi, puoi vederlo se scrivi una semplice espressione matematica

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

e il risultato è W > 2 * N, quindi è necessario aumentare il criterio RHS di un bit a (n-m+1). A questo punto, l'operazione è sicura fino a quando W < 4; se non funziona, aumentare di nuovo il criterio, ecc.

Penso che seguire quanto sopra ti porterà molto lontano per la tua risposta ...


1
Grande. Un altro problema sottile: il test m-1 / nm fallisce qualche volta a causa dei bit di riporto. Prova a ... b..c ... d - ti ritrovi con b + c nel quinto bit che, se sono entrambi 1, fa un pezzetto che porta a clobber d (!)
Ternary,

1
risultato: n-1 bit di spazio proibisce le configurazioni che dovrebbero funzionare (iea ... b..c ... d), e m-1 / nm consente quelle che non funzionano (a ... b..c ... d). Non sono stato in grado di trovare un modo semplice per caratterizzare chi funzionerà e quali no.
Ternary,

Sei bravo! Il problema del carry significa che abbiamo bisogno di un po 'più di spazio a destra di ogni bit come "protezione". A prima vista, se ci sono almeno due bit che hanno esattamente il minimo nm a destra, è necessario aumentare lo spazio di 1. Più in generale, se ci sono P tali bit, sono necessari log2 (P) bit aggiuntivi per diritto di qualsiasi che avesse il minimo (mn). Ti sembra giusto?
Floris,

Bene, quell'ultimo commento era troppo semplicistico. Penso che la mia risposta modificata più di recente mostri che log2 (P) non è l'approccio giusto. La risposta di @ Ternary (sotto) mostra elegantemente come si può dire per una particolare combinazione di bit se non si dispone di una soluzione garantita - credo che il lavoro sopra descritto ne approfondisca ancora un po '.
Floris,

1
Probabilmente è una coincidenza, ma questa risposta è stata accettata quando il numero di voti ha raggiunto 127. Se hai letto fino a qui, sorriderai con me ...
Floris,

154

Domanda davvero interessante. Sto rispondendo con i miei due centesimi, il che è che, se riesci a dichiarare problemi come questo in termini di logica di primo ordine sulla teoria dei bitvector, allora i dimostratori di teoremi sono tuoi amici e possono potenzialmente fornirti molto velocemente risposte alle tue domande. Ribadiamo il problema che viene chiesto come teorema:

"Esistono alcune costanti a 64 bit 'maschera' e 'multiplicando' tali che, per tutti i bitvector a 64 bit x, nell'espressione y = (x & maschera) * multiplicando, abbiamo che y.63 == x.63 , y.62 == x.55, y.61 == x.47, ecc. "

Se questa frase è in realtà un teorema, allora è vero che alcuni valori delle costanti 'maschera' e 'multiplicando' soddisfano questa proprietà. Quindi diciamo questo in termini di qualcosa che un proverbi teorema può capire, vale a dire l'input 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)

E ora chiediamo al proverore di teoremi Z3 se questo è un teorema:

z3.exe /m /smt2 ExtractBitsThroughAndWithMultiplication.smt2

Il risultato è:

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

Bingo! Riproduce il risultato fornito nel post originale in 0,06 secondi.

Guardando questo da una prospettiva più generale, possiamo vederlo come un'istanza di un problema di sintesi del programma di primo ordine, che è una nascente area di ricerca su cui sono stati pubblicati pochi articoli. Una ricerca per "program synthesis" filetype:pdfiniziare.


2
Sono impressionato! Non sapevo che la "logica del primo ordine sulla teoria dei bitvector" fosse persino un argomento reale che la gente studiava - figuriamoci sul fatto che potesse dare risultati così interessanti. Grazie mille per averlo condiviso.
Floris,

@AndrewBacker: Qualcuno potrebbe illuminarmi su quale punto ci sia in questa cosiddetta cosa "SO-as-a-job"? Voglio dire, non paga nulla. Non puoi vivere da solo con un rappresentante SO. Forse può darti alcuni punti nelle interviste. Può essere. Se il posto di lavoro è abbastanza buono da riconoscere il valore del rappresentante SO, e non è un dato di fatto ...
Ripristina Monica il

3
Sicuro. SO è anche un gioco (qualsiasi cosa abbia punti) per molte persone. Solo la natura umana, come cacciare in / r / new in modo da poter pubblicare il primo commento e ottenere karma. Niente di male, a condizione che le risposte siano ancora buone. Sono solo più felice di essere in grado di valutare il tempo e lo sforzo di qualcuno quando è probabile che notino effettivamente che qualcuno lo ha fatto. L'incoraggiamento è roba buona :) E ... quello era un commento davvero vecchio, e ancora vero. Non vedo come non sia chiaro.
Andrew Backer,

88

Ogni 1 bit nel moltiplicatore viene utilizzato per copiare uno dei bit nella posizione corretta:

  • 1è già nella posizione corretta, quindi moltiplicare per 0x0000000000000001.
  • 2 deve essere spostato di 7 bit a sinistra, quindi moltiplichiamo per 0x0000000000000080 (il bit 7 è impostato).
  • 3 deve essere spostato a sinistra di 14 bit, quindi moltiplichiamo per 0x0000000000000400 (il bit 14 è impostato).
  • e così via fino a
  • 8 deve essere spostato di 49 bit a sinistra, quindi moltiplichiamo per 0x0002000000000000 (il bit 49 è impostato).

Il moltiplicatore è la somma dei moltiplicatori per i singoli bit.

Questo funziona solo perché i bit da raccogliere non sono troppo vicini tra loro, in modo che la moltiplicazione dei bit che non appartengono al nostro schema cada oltre i 64 bit o nella parte non importa inferiore.

Si noti che gli altri bit nel numero originale devono essere 0. Ciò può essere ottenuto mascherandoli con un'operazione AND.


2
Grande spiegazione! La tua breve risposta ha permesso di trovare rapidamente il valore del "numero magico".
Expedito,

4
Questa è davvero la risposta migliore, ma non sarebbe stato così utile senza prima leggere (la prima metà) della risposta di @ floris.
Andrew Backer,

29

(Non l'avevo mai visto prima. Questo trucco è fantastico!)

Espanderò un po 'l'affermazione di Floris secondo cui quando si estraggono i nbit è necessario n-1spazio tra i bit non consecutivi:

Il mio pensiero iniziale (vedremo tra un minuto come questo non funziona del tutto) è che potresti fare di meglio: se vuoi estrarre nbit, avrai una collisione durante l'estrazione / spostamento di bit ise hai qualcuno (non -consecutivo con bit i) nei i-1bit precedenti on-i successivi.

Darò alcuni esempi per illustrare:

...a..b...c...Funziona (nessuno nei 2 bit dopo a, il bit prima e il bit dopo b, e nessuno è nei 2 bit prima c):

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

...a.b....c...Non riesce perché si btrova nei 2 bit dopo a(e viene trascinato nel posto di qualcun altro quando cambiamo a):

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

...a...b.c...Non riesce perché si btrova nei 2 bit precedenti c(e viene spostato nel posto di qualcun altro quando spostiamo c):

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

...a...bc...d... Funziona perché i bit consecutivi si spostano insieme:

  a000bc000d
+ 0bc000d000
+ 000d000000
= abcd000000

Ma abbiamo un problema. Se usiamo n-iinvece di n-1potremmo avere il seguente scenario: cosa succede se abbiamo una collisione al di fuori della parte che ci interessa, qualcosa che maschereremmo alla fine, ma i cui bit di trasporto finiscono per interferire nell'importante intervallo non mascherato ? (e nota: il n-1requisito assicura che ciò non accada assicurandosi che i i-1bit dopo il nostro intervallo non mascherato siano chiari quando spostiamo il ibit th)

...a...b..c...d...Il potenziale guasto sui bit di riporto cè in n-1after b, ma soddisfa i n-icriteri:

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

Allora perché non torniamo a quel n-1requisito " bit di spazio"? Perché possiamo fare di meglio :

...a....b..c...d.. Non riesce il n-1test " bit di spazio", ma funziona per il nostro trucco per l'estrazione di bit:

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

Non riesco a trovare un buon modo per caratterizzare questi campi che non hanno n-1spazio tra bit importanti, ma funzionerebbero comunque per la nostra operazione. Tuttavia, poiché sappiamo in anticipo quali bit ci interessano, possiamo controllare il nostro filtro per assicurarci di non riscontrare collisioni di carry-bit:

Confronto (-1 AND mask) * shiftcon il risultato atteso di tutti, -1 << (64-n)(per 64-bit senza segno)

Lo spostamento / moltiplicazione magica per estrarre i nostri bit funziona se e solo se i due sono uguali.


Mi piace - hai ragione che per ogni bit, hai solo bisogno di tanti zeri a destra di esso quanto hai bisogno di spazio per i bit che devono andare lì. Inoltre , ha bisogno di tanti bit a sinistra quanti bit di risultato a sinistra. Quindi, se un po ' bfinisce in posizione mdi n, allora deve avere degli m-1zeri alla sua sinistra e degli n-m-1zeri alla sua destra. Soprattutto quando i bit non sono nello stesso ordine nel numero originale come saranno dopo il riordino, questo è un miglioramento importante dei criteri originali. Questo è divertente.
Floris,

13

Oltre alle già eccellenti risposte a questa domanda molto interessante, potrebbe essere utile sapere che questo trucco di moltiplicazione bit per bit è noto nella comunità degli scacchi del computer dal 2007, dove va sotto il nome di Magic BitBoards .

Molti motori di scacchi per computer usano diversi numeri interi a 64 bit (chiamati bitboard) per rappresentare i vari set di pezzi (1 bit per quadrato occupato). Supponiamo che un pezzo scorrevole (torre, vescovo, regina) su un certo quadrato di origine possa spostarsi nella maggior parte dei Kquadrati se non erano presenti pezzi bloccanti. L'uso di bit a bit e di quei Kbit sparsi con il bitboard dei quadrati occupati fornisce una Kparola a bit specifico incorporata in un numero intero a 64 bit.

La moltiplicazione magica può essere utilizzata per mappare questi Kbit sparsi ai bit inferiori Kdi un numero intero a 64 bit. Questi inferioriK bit possono quindi essere utilizzati per indicizzare una tabella di bitboard pre-calcolati che rappresentano i quadrati consentiti in cui il pezzo sul suo quadrato di origine può effettivamente spostarsi (occupandosi di bloccare i pezzi ecc.)

Un tipico motore di scacchi che utilizza questo approccio ha 2 tabelle (una per torri, una per vescovi, regine che usano la combinazione di entrambi) di 64 voci (una per quadrato di origine) che contengono tali risultati pre-calcolati. Sia il motore di scacchi open source ( Houdini ) che il più alto rating ( Stoccafisso ) attualmente utilizzano questo approccio per le sue altissime prestazioni.

La ricerca di questi moltiplicatori magici viene effettuata utilizzando una ricerca esaustiva (ottimizzata con i tagli iniziali) o con prove ed errori (ad esempio, provando molti numeri casuali a 64 bit). Durante la generazione delle mosse non sono stati utilizzati schemi di bit per i quali non è stata trovata alcuna costante magica. Tuttavia, gli effetti di trasporto bit a bit sono in genere necessari quando i bit da mappare hanno indici (quasi) adiacenti.

AFAIK, il solutore SAT generale proposto da @Syzygy non è stato usato negli scacchi per computer, e non sembra esserci alcuna teoria formale sull'esistenza e l'unicità di tali costanti magiche.


Avrei pensato che chiunque avesse avuto un background CS formale sarebbe saltato sull'approccio SAT direttamente dopo aver visto questo problema. Forse le persone CS trovano gli scacchi poco interessanti? :(
Ripristina Monica il

@KubaOber È principalmente il contrario: gli scacchi per computer sono dominati da bit-twiddlers che programmano in C o assembly e odiano ogni tipo di astrazione (C ++, template, OO). Penso che spaventa i veri ragazzi CS :-)
TemplateRex
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.