31 votes

"Isoler" une rangée, une colonne ou une diagonale spécifique d'un nombre de 64 bits.

OK, considérons un nombre de 64 bits, dont les bits forment une table 8x8.

Par exemple

0 1 1 0 1 0 1 0 0 1 1 0 1 0 1 1 0 1 1 1 1 0 1 0 0 1 1 0 1 0 1 0 1 1 1 0 1 0 1 0 0 1 1 0 1 0 1 0 0 1 1 0 1 1 1 0 0 1 1 0 1 0 1 0

écrit comme suit

a b c d e f g h
----------------
0 1 1 0 1 0 1 0
0 1 1 0 1 0 1 1 
0 1 1 1 1 0 1 0 
0 1 1 0 1 0 1 0 
1 1 1 0 1 0 1 0 
0 1 1 0 1 0 1 0 
0 1 1 0 1 1 1 0 
0 1 1 0 1 0 1 0

Maintenant, que se passe-t-il si nous voulons isoler JUST, par exemple. colonne d ( 00100000 ) (ou toute ligne/diagonale d'ailleurs) ?

Cela peut-il être fait ? Et si oui, comment ?


CONSEILS :

  • (a) Mon principal objectif ici - bien qu'il n'ait pas été mentionné au départ - est la vitesse brute. Je cherche l'algorithme le plus rapide, puisque la fonction de "récupération" est exécutée des millions de fois. par seconde .

  • (b) Voici ce qui se rapproche le plus de ce que je veux dire : https://www.chessprogramming.org/Kindergarten_Bitboards

4 votes

Est-ce que vous venez vraiment de demander "est-ce que c'est faisable" ? Vous attendez-vous sérieusement à ce que la réponse soit "non, c'est totalement impossible" ?

1 votes

@KerrekSB Non, en fait je demande comment ... lol.

1 votes

Bien sûr que c'est possible. Pour ce faire, il faut un peu de décalage, et et ou. En fonction de ce que vous voulez réellement faire [ou est-ce un exercice scolaire ?], il peut y avoir différents types de "trucs intelligents" qui peuvent être appliqués, mais les décalages, les et et les ou sont probablement ce que vous voulez. Voulez-vous que je poste une réponse avec cela ?

62voto

viraptor Points 12779

Voici une solution qui ne comporte que 4 étapes principales :

const uint64_t column_mask = 0x8080808080808080ull;
const uint64_t magic = 0x2040810204081ull;

int get_col(uint64_t board, int col) {
    uint64_t column = (board << col) & column_mask;
    column *= magic;
    return (column >> 56) & 0xff;
}

Cela fonctionne comme suit :

  • la planche est décalée pour aligner la colonne avec le côté gauche
  • il est masqué pour ne contenir que la colonne requise (0..8)
  • il est multiplié par un nombre magique qui fait que tous les bits originaux sont poussés vers la gauche.
  • l'octet le plus à gauche est décalé vers la droite.

Le nombre magique est choisi pour ne copier que les bits nécessaires et laisser le reste tomber dans des endroits inutilisés / déborder sur le nombre. Le processus ressemble à ceci (les chiffres sont des "ID" de bits, plutôt que le nombre lui-même) :

original column: ...1.......2.......3.......4.......5.......6.......7.......8....
aligned column:  1.......2.......3.......4.......5.......6.......7.......8.......
multiplied:      123456782345678.345678..45678...5678....678.....78......8.......
shifted to right:........................................................12345678

Si vous ajoutez le const mots-clés, l'assemblage devient assez agréable en fait :

get_col:
.LFB7:
        .cfi_startproc
        movl    %esi, %ecx
        movabsq $-9187201950435737472, %rax
        salq    %cl, %rdi
        andq    %rax, %rdi
        movabsq $567382630219905, %rax
        imulq   %rax, %rdi
        shrq    $56, %rdi
        movl    %edi, %eax
        ret

Pas de branchement, pas de données externes, environ 0,4ns par calcul.

Edit : prend environ 6 fois plus de temps en utilisant la solution de NPE comme ligne de base (la prochaine plus rapide)

5 votes

(+1) Magnifique. Pourriez-vous nous expliquer le processus qui a permis d'arriver à ce chiffre magique ? Merci.

6 votes

J'ai utilisé un peu de python pour inspecter les résultats et essayer d'arriver "visuellement" à la bonne solution. En gros, j'ai pensé à la multiplication binaire comme "shift+copy, répéter pour chaque ensemble de bits". Alors le bit "1" a besoin de magic="1", les bits "12" ont besoin de magic="10000001", etc. puis le schéma se répète. Le nombre magique est 10000001000000100000010000001000000100000010000001 .

1 votes

C'est ce que j'appelle une bonne réponse. Merci à tous ceux qui ont consacré du temps à ma question ! :-)

7voto

Mats Petersson Points 70074

Bon, alors pour "trancher" le débat sur ce qui est plus rapide/plus lent/etc, j'ai mis tout le code dans un programme [et j'ai espoir J'ai crédité la bonne personne pour le bon extrait de code].

Le code peut être trouvé ci-dessous, pour l'inspection que j'ai intepreded le code correctement quand je l'ai fait dans des fonctions. Je l'ai exécuté sans sortie correcte et j'ai vérifié que chaque fonction donne le même résultat [en gardant à l'esprit que l'ordre est légèrement différent dans certains cas - j'ai donc fait une variation pour exécuter l'autre sens de mon code, juste pour voir qu'il donne le "bon" résultat]. Donc sans plus attendre, voici les résultats :

mats1 time in clocks per iteration 10.3457
mats2 time in clocks per iteration 10.4785
mats3 time in clocks per iteration 10.5538
viraptor time in clocks per iteration 6.24603
lemees time in clocks per iteration 14.4818
npe time in clocks per iteration 13.1455
alex time in clocks per iteration 24.8272

(résultats de viraptor sur core i5, g++ 4.7)

mats1 time in clocks per iteration 7.62338
mats2 time in clocks per iteration 7.36226
mats3 time in clocks per iteration 7.45361
viraptor time in clocks per iteration 2.09582
lemees time in clocks per iteration 9.43744
npe time in clocks per iteration 7.51016
alex time in clocks per iteration 19.3554

(résultats de viraptor sur core i5, clang++ 3.2)

mats1 time in clocks per iteration 12.956
mats2 time in clocks per iteration 13.4395
mats3 time in clocks per iteration 13.3178
viraptor time in clocks per iteration 2.12914
lemees time in clocks per iteration 13.9267
npe time in clocks per iteration 16.2102
alex time in clocks per iteration 13.8705

Ce sont des cycles d'horloge sur un AMD Athlon2 à 3,4 GHz - je n'ai pas de machine Intel moderne - si quelqu'un souhaite exécuter le code sur cette machine, je serais intéressé de voir à quoi cela ressemble. Je suis presque sûr que tout fonctionne bien dans le cache - peut-être à part récupérer certaines des valeurs pour les vérifier.

Ainsi, le gagnant est clairement viraptor, d'environ 40% - "mon" code est deuxième. Le code d'Alex n'a pas de sauts/branches, mais il semble toujours fonctionner plus lentement que les autres alternatives. Je ne sais pas pourquoi les résultats de npe sont beaucoup plus lents que les miens - il fait presque la même chose (et le code semble très similaire quand on regarde la sortie assembleur de g++).

#include <iostream>
#include <fstream>
#include <cstdint>

using namespace std;

const int SIZE = 1000000;

uint64_t g_val[SIZE];

ofstream nulloutput;

static __inline__ unsigned long long rdtsc(void)
{
    unsigned hi, lo;
    __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
    return ( (unsigned long long)lo)|( ((unsigned long long)hi)<<32 );
}

#define BITA_TO_B(x, a, b) (((x) >> (a-b)) & (1 << b))

unsigned char get_col_mats1(uint64_t val, int col)
{
    return BITA_TO_B(val, 56+col, 7) |
    BITA_TO_B(val, 48+col, 6) |
    BITA_TO_B(val, 40+col, 5) |
    BITA_TO_B(val, 32+col, 4) |
    BITA_TO_B(val, 24+col, 3) |
    BITA_TO_B(val, 16+col, 2) |
    BITA_TO_B(val, 8+col, 1) |
    BITA_TO_B(val, 0+col, 0);
}

unsigned char get_col_mats2(uint64_t val, int col)
{
    return BITA_TO_B(val, 63-col, 7) |
    BITA_TO_B(val, 55-col, 6) |
    BITA_TO_B(val, 47-col, 5) |
    BITA_TO_B(val, 39-col, 4) |
    BITA_TO_B(val, 31-col, 3) |
    BITA_TO_B(val, 23-col, 2) |
    BITA_TO_B(val, 15-col, 1) |
    BITA_TO_B(val, 7-col, 0);
}

unsigned char get_col_viraptor(uint64_t board, int col) {
    const uint64_t column_mask = 0x8080808080808080ull;
    const uint64_t magic = 0x2040810204081ull ;
    uint64_t column = board & (column_mask >> col);
    column <<= col;
    column *= magic;
    return (column >> 56) & 0xff;
}

unsigned char get_col_alex(uint64_t bitboard, int col)
{
    unsigned char result;
    result |= (bitboard & (1ULL << 63-col)) ? 0x80 : 0;
    result |= (bitboard & (1ULL << 55-col)) ? 0x40 : 0;
    result |= (bitboard & (1ULL << 47-col)) ? 0x20 : 0;
    result |= (bitboard & (1ULL << 39-col)) ? 0x10 : 0;
    result |= (bitboard & (1ULL << 31-col)) ? 0x08 : 0;
    result |= (bitboard & (1ULL << 23-col)) ? 0x04 : 0;
    result |= (bitboard & (1ULL << 15-col)) ? 0x02 : 0;
    result |= (bitboard & (1ULL << 7-col))  ? 0x01 : 0;

    return result;
}

unsigned char get_col_lemees(uint64_t val, int column)
{
    int result = 0;
    int source_bitpos = 7 - column; // "point" to last entry in this column
    for (int target_bitpos = 0; target_bitpos < 8; ++target_bitpos)
    {
    bool bit = (val >> source_bitpos) & 1;  // "extract" bit
    result |= bit << target_bitpos;            // add bit if it was set
    source_bitpos += 8;                        // move one up in table
    }
    return result;
}

int get(uint64_t board, int row, int col) {
  return (board >> (row * 8 + col)) & 1;
}

uint8_t get_col_npe(uint64_t board, int col) {
  uint8_t ret = 0;
  for (int i = 0; i < 8; ++i) {
    ret = (ret << 1) + get(board, i, col);
  }
  return ret;
}

#define BITA_TO_B2(x, a, b) (((x) >> (a-b)) & (1 << b))

unsigned char get_col_mats3(uint64_t val, int col)
{
    return BITA_TO_B2(val, 63-col, 7) |
    BITA_TO_B2(val, 55-col, 6) |
    BITA_TO_B2(val, 47-col, 5) |
    BITA_TO_B2(val, 39-col, 4) |
    BITA_TO_B2(val, 31-col, 3) |
    BITA_TO_B2(val, 23-col, 2) |
    BITA_TO_B2(val, 15-col, 1) |
    BITA_TO_B2(val, 7-col, 0);
}

template<unsigned char (*f)(uint64_t val, int col)>
void runbench(const char *name)
{
    unsigned char col[8]  = {0};
    uint64_t long t = rdtsc();
    for(int j = 0; j < SIZE; j++)
    {
    uint64_t val = g_val[j];
    for(int i = 0; i < 8; i++)
    {
        col[i] += f(val, i);
    }
//  __asm__ __volatile__("":::"memory");
    }
    t = rdtsc() - t;
    for(int i = 0; i < 8; i++)
    {
    nulloutput<< "col " << i << " has bits " << hex << (int)col[i] << endl;
    }
    cout << name << " time in clocks per iteration " << dec << t / (8.0 * SIZE) << endl;
}

#define BM(name) void bench_##name() { runbench<get_col_##name>(#name); }

BM(mats1);
BM(mats2);
BM(mats3);
BM(viraptor);
BM(lemees);
BM(npe);
BM(alex);

struct function
{
    void (*func)(void);
    const char *name;
};

#define FUNC(f) { bench_##f, #f }

function funcs[] = 
{
    FUNC(mats1),
    FUNC(mats2),
    FUNC(mats3),
    FUNC(viraptor),
    FUNC(lemees),
    FUNC(npe),
    FUNC(alex),
}; 

int main()
{
    unsigned long long a, b;
    int i;
    int sum = 0;

    nulloutput.open("/dev/nul");
    for(i = 0; i < SIZE; i++)
    {
    g_val[i] = rand() + ((long)rand() << 32L);
    }
    unsigned char col[8];

    for(i = 0; i < sizeof(funcs)/sizeof(funcs[0]); i++)
    {
    funcs[i].func();
    }
}

0 votes

J'allais essayer de l'exécuter avec clang également, mais pour une raison quelconque, clang n'aime pas iostream et se plante. Il ne faisait pas ça hier ! :(

1 votes

Merci d'avoir rassemblé les solutions. J'espère que la modification ne vous dérange pas - j'ai ajouté les résultats de core i5 gcc et clang.

0 votes

Bien. Intéressant que clang ne soit pas aussi bon que gcc - la plupart des gens semblent aimer clang...

2voto

NPE Points 169956

Codez-le avec des boucles simples et laissez l'optimiseur faire l'inlining et le déroulage des boucles pour vous.

Compilé en utilisant 4.7.2 avec -O3 sur ma boîte les suivants peuvent réaliser environ 300 millions get_col() appels par seconde.

bitboard.cpp:

#include <cinttypes>
#include <iostream>

int get(uint64_t board, int row, int col) {
  return (board >> (row * 8 + col)) & 1;
}

uint8_t get_col(uint64_t board, int col) {
  uint8_t ret = 0;
  for (int i = 0; i < 8; ++i) {
    ret = (ret << 1) + get(board, i, col);
  }
  return ret;
}

extern uint64_t board;
extern int sum;

extern void f();

int main() {
  for (int i = 0; i < 40000000; ++i) {
    for (int j = 0; j < 8; ++j) {
      sum += get_col(board, j);
    }
    f();
  }
  std::cout << sum << std::endl;
}

bitboard_b.cpp:

#include <cinttypes>

uint64_t board = 0x1234567890ABCDEFull;
int sum = 0;

void f() {}

Si vous regardez le code d'assemblage pour get_col() vous verrez qu'il ne contient aucune boucle et qu'il est probablement aussi efficace que tout ce que vous êtes susceptible de faire à la main :

__Z7get_colyi:
LFB1248:
        movl    %esi, %ecx
        movq    %rdi, %rax
        movq    %rdi, %rdx
        shrq    %cl, %rax
        leal    8(%rsi), %ecx
        andl    $1, %eax
        shrq    %cl, %rdx
        leal    16(%rsi), %ecx
        andl    $1, %edx
        leal    (%rdx,%rax,2), %eax
        movq    %rdi, %rdx
        shrq    %cl, %rdx
        leal    24(%rsi), %ecx
        andl    $1, %edx
        leal    (%rdx,%rax,2), %eax
        movq    %rdi, %rdx
        shrq    %cl, %rdx
        leal    32(%rsi), %ecx
        andl    $1, %edx
        leal    (%rdx,%rax,2), %eax
        movq    %rdi, %rdx
        shrq    %cl, %rdx
        leal    40(%rsi), %ecx
        andl    $1, %edx
        leal    (%rdx,%rax,2), %edx
        movq    %rdi, %rax
        shrq    %cl, %rax
        leal    48(%rsi), %ecx
        andl    $1, %eax
        leal    (%rax,%rdx,2), %edx
        movq    %rdi, %rax
        shrq    %cl, %rax
        leal    56(%rsi), %ecx
        andl    $1, %eax
        leal    (%rax,%rdx,2), %eax
        shrq    %cl, %rdi
        andl    $1, %edi
        leal    (%rdi,%rax,2), %eax
        ret

Il ne s'agit pas d'une mise en œuvre complète, mais d'une illustration grossière de l'idée. En particulier, l'ordre des bits peut être à l'opposé de ce que vous attendez, etc.

0 votes

Qu'est-ce que c'est que ce NOOP f() demander ? Empêcher l'optimisation ?

0 votes

@leemes : Oui. Sinon gcc optimise le 40000000 boucle en multipliant le résultat de la boucle interne par 40000000 .

0 votes

Est ret = (ret << 1) + ... aussi vite que ret |= ... << i ? Pouvez-vous s'il vous plaît comparer, maintenant que vous avez déjà fait un petit benchmark ici ;)

1voto

leemes Points 17391

Dans votre cas (spécialisé pour les tables 8x8 = 64 bits), vous devez effectuer un bithifting pour extraire les bits spécifiques et les réorganiser dans une nouvelle variable entière, en utilisant également le bithifting :

uint64_t matrix = ... // input
int column = 3; // "d"-column

int result = 0;
int source_bitpos = 7 - column; // "point" to last entry in this column
for (int target_bitpos = 0; target_bitpos < 8; ++target_bitpos)
{
    bool bit = (matrix >> source_bitpos) & 1;  // "extract" bit
    result |= bit << target_bitpos;            // add bit if it was set
    source_bitpos += 8;                        // move one up in table
}

Voir ici : http://ideone.com/UlWAAO

0 votes

Ne serait-il pas plus simple de masquer les bits puis de déplacer le tout ?

0 votes

@juanchopanza Puisque la matrice est donnée ligne-major et qu'il demande une colonne, il faut réarranger les bits. Ou alors j'ai mal interprété la question ?

1voto

Mats Petersson Points 70074
#define BITA_TO_B(x, a, b) (((x) >> (a)) & 1) << b;

unsigned long long x = 0x6A6B7A6AEA6E6A;

unsigned char col_d = BITA_TO_B(x, 60, 7) |
                      BITA_TO_B(x, 52, 6) |
                      BITA_TO_B(x, 44, 5) |
                      BITA_TO_B(x, 36, 4) |
                      BITA_TO_B(x, 28, 3) |
                      BITA_TO_B(x, 20, 2) |
                      BITA_TO_B(x, 12, 1) |
                      BITA_TO_B(x,  4, 0);

Peut-être une idée un peu plus optimisée :

#define BITA_TO_B(x, a, b) (((x) >> (a-b)) & (1 << b));

Si b est une constante, les résultats seront légèrement meilleurs.

Un autre moyen peut être :

unsigned long xx = x & 0x10101010101010; 
col_d = (xx >> 53) | (xx >> 46) | (xx >> 39) ... (xx >> 4); 

Faire un "et" plutôt que plusieurs permet d'accélérer le processus.

0 votes

J'espérais que le compilateur résoudrait ce problème. Bien sûr, on pourrait l'optimiser pour produire le bon bit au bon endroit. Modification à venir.

0 votes

Il est possible de le faire sans aucun décalage.

0 votes

Est-ce que c'est une astuce intelligente ? (Et ne pas utiliser "multiplier" ou "diviser", qui, j'en suis sûr, est pire que les décalages).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X