Подтвердить что ты не робот

Получить массив позиций битов в 64-битном целочисленном

ОК, это может показаться немного сложным, но это то, что я пытаюсь сделать:

  • Возьмите, например. 10101010101
  • И верните { 0, 2, 4, 6, 8, 10 } - массив со всеми позициями битов, которые установлены

Это мой код:

UINT DQBitboard::firstBit(U64 bitboard)
{
    static const int index64[64] = {
    63,  0, 58,  1, 59, 47, 53,  2,
    60, 39, 48, 27, 54, 33, 42,  3,
    61, 51, 37, 40, 49, 18, 28, 20,
    55, 30, 34, 11, 43, 14, 22,  4,
    62, 57, 46, 52, 38, 26, 32, 41,
    50, 36, 17, 19, 29, 10, 13, 21,
    56, 45, 25, 31, 35, 16,  9, 12,
    44, 24, 15,  8, 23,  7,  6,  5  };

    static const U64 debruijn64 = 0x07EDD5E59A4E28C2ULL;

    #pragma warning (disable: 4146)
    return index64[((bitboard & -bitboard) * debruijn64) >> 58];  
}

vector<UINT> DQBitboard::bits(U64 bitboard)
{
    vector<UINT> res;

    while (bitboard)
    {
        UINT first = DQBitboard::firstBit(bitboard);
        res.push_back(first);

        bitboard &= ~(1ULL<<first);
    }

    return res;
}

И код, безусловно, работает.

Моя точка:

  • Есть ли какая-то более быстрая реализация, которую вы имеете в виду?
  • Вы заметили что-нибудь, что можно было бы оптимизировать? Если да, то что?

Советы:

  • UINT - это typedef unsigned int
  • U64 - это typedef unsigned long long
  • Оба метода: static inline.
4b9b3361

Ответ 1

Вот еще одно предложение, которое можно профилировать (можно комбинировать с другими предложениями для дальнейшей оптимизации). Обратите внимание: цикл здесь O(number of set bits).

vector<UINT> bits_set (UINT64 data) 
{
    UINT n;
    vector<UINT> res;
    res.reserve(64);
    for (n = 0; data != 0; n++, data &= (data - 1))
    {
        res.push_back(log2(data & ~(data-1)));
    }
    return res;
}

Ответ 2

Бит-сдвиги действительно дешевы. В таблицах Lookup tables используется кеш-память, и ваш поиск также имеет целочисленное умножение. Я думаю, что просто грубая сила будет быстрее, чем умные методы.

vector<UINT> DQBitboard::bits(U64 bitboard)
{
    vector<UINT> res;
    res.reserve(64);
    uint_fast8_t pos = 1;

    do {
        if (bitboard & 1) res.push_back(pos);
        ++pos;
    } while (bitboard >>= 1);

    return res;
}

Вы можете немного развернуть цикл, что может сделать его еще быстрее.

std::vector - самая дорогая часть. Подумайте о том, как использовать битовую плату напрямую. Например:

struct bitboard_end_iterator{};
struct bitboard_iterator
{
    U64 value;
    uint_fast8_t pos;

    bitboard_iterator(U64 bitboard) : value(bitboard), pos(0)
    {
        ++(*this);
    }
    UINT operator*() const { return pos + 1; }
    bool operator==( bitboard_end_iterator ) const { return pos == 64; }
    operator bool() const { return pos < 64; }
    bitboard_iterator& operator++()
    {
        while (U64 prev = value) {
            value >>= 1;
            ++pos;
            if (prev & 1) return *this;
        }
        pos = 64;
        return *this;
    }
};

Теперь вы можете написать

for( bitboard_iterator it(bitboard); it; ++it )
    cout << *it;

и я думаю, вы получите список бит.

Версия 2:

class bitboard_fast_iterator
{
    U64 value;
    uint_fast8_t pos;

public:
    bitboard_fast_iterator(U64 bitboard = 0) : value(bitboard), pos(__builtin_ctzll(value)) {}
    UINT operator*() const { return pos + 1; }
    operator bool() const { return value != 0; }
    bitboard_iterator& operator++()
    {
        value &= ~(1ULL << pos);
        pos = __builtin_ctzll(value);
        return *this;
    }
};

Ответ 3

Мне было интересно, будет ли он работать быстрее с инструкцией сборки bst. Поэтому я попробовал 3 реализации и получил следующие результаты для 10 миллионов итераций:

Ваша реализация (Dr.Kameleon) 1,77 секунды

Реализация log2() (icepack) 2.17 секунд

Моя реализация сборки (меня) 0.16 секунд

Вывод:

bits version:
Function started at 0
           ended at 177
              spent 177 (1.770000 seconds)
c version:
Function started at 177
           ended at 394
              spent 217 (2.170000 seconds)
c version:
Function started at 394
           ended at 410
              spent 16 (0.160000 seconds)

Один момент о C/С++, статический - это ужасно. Он фактически скомпилирован в списке инструкций CPU (НЕ того, чего я ожидал бы!!!) Вместо этого, используйте массив вне вашей функции в безымянном пространстве имен. Это будет иметь ожидаемый эффект. Хотя в сборке вы можете использовать .long(или какой-то другой размер), а затем% rip для ссылки на данные с IP.

Примечание: после компиляции я не вижу, что размер (n) используется в моей версии сборки, поэтому я не уверен, верен ли массив. Вне этого сам код становится петлей из 5 инструкций сборки, следовательно, крошечное увеличение скорости (около x10).

Причиной ошибки log2() является то, что она преобразует число в регистр xmm и затем вызывает другую функцию. Затем он преобразует регистр xmm в обычный регистр.

#include <stdlib.h>
#include <stdio.h>
#include <inttypes.h>
#include <unistd.h>
#include <sys/times.h>
#include <string.h>
#include <math.h>
#include <vector>

using namespace std;

namespace
{
const int index64[64] = {
    63,  0, 58,  1, 59, 47, 53,  2,
    60, 39, 48, 27, 54, 33, 42,  3,
    61, 51, 37, 40, 49, 18, 28, 20,
    55, 30, 34, 11, 43, 14, 22,  4,
    62, 57, 46, 52, 38, 26, 32, 41,
    50, 36, 17, 19, 29, 10, 13, 21,
    56, 45, 25, 31, 35, 16,  9, 12,
    44, 24, 15,  8, 23,  7,  6,  5  };
const uint64_t debruijn64 = 0x07EDD5E59A4E28C2ULL;
}

int firstBit(uint64_t bitboard)
{
    return index64[((bitboard & -bitboard) * debruijn64) >> 58];  
}

vector<int> bits(uint64_t bitboard)
{
    vector<int> res;
    res.reserve(64);

    while(bitboard)
    {
        int first = firstBit(bitboard);
        res.push_back(first);

        bitboard &= ~(1ULL << first);
    }
    return res;
}



vector<int> bits_c(uint64_t bitboard)
{
    int n;
    vector<int> res;
    res.reserve(64);
    for (n = 0; bitboard != 0; n++, bitboard &= (bitboard - 1))
    {
        res.push_back(log2(bitboard & ~(bitboard - 1)));
    }
    return res;
}


vector<int> bits_asm(uint64_t bitboard)
{
    int64_t n(0);
    int res[64];
    asm(
    "bsf %[b], %%rax\n\t"
    "je exit\n\t"
    ".align 16\n"
"loop:\n\t"
    "mov %%eax, (%[r],%[n],4)\n\t"
    "btr %%rax, %[b]\n\t"
    "inc %[n]\n\t"
    "bsf %[b], %%rax\n\t"
    "je loop\n"
"exit:\n\t"
    : /* output */ "=r" (n)
    : /* input */ [n] "r" (n), [r] "r" (res), [b] "r" (bitboard)
    : /* state */ "eax", "cc"
    );
    return vector<int>(res, res + n);
}




class run_timer
{
public:
    run_timer()
    {
    }

    void start()
    {
        times(&f_start);
    }

    void stop()
    {
        times(&f_stop);
    }

    void report(const char *msg)
    {
        printf("%s:\n"
               "Function started at %ld\n"
               "           ended at %ld\n"
               "              spent %ld (%f seconds)\n",
               msg,
               f_start.tms_utime,
               f_stop.tms_utime,
               f_stop.tms_utime - f_start.tms_utime,
               (double)(f_stop.tms_utime - f_start.tms_utime)/(double)sysconf(_SC_CLK_TCK));
    }

    struct tms f_start;
    struct tms f_stop;
};


int main(int argc, char *argv[])
{
    run_timer t;

    t.start();
    for(int i(0); i < 10000000; ++i)
    {
        bits(rand());
    }
    t.stop();
    t.report("bits version");

    t.start();
    for(int i(0); i < 10000000; ++i)
    {
        bits_c(rand());
    }
    t.stop();
    t.report("c version");

    t.start();
    for(int i(0); i < 10000000; ++i)
    {
        bits_asm(rand());
    }
    t.stop();
    t.report("c version");

    return 0;
}

Скомпилирован с g++ с этой командной строкой:

c++ -msse4.2 -O2 -o bits -c bits.cpp

Хотя вы можете подумать, что проблема -msse4.2 может быть проблемой с версией log2(), я пробовал без нее, а log2() работает медленнее.

Btw, я не рекомендую этот метод, так как он не переносится. Эти компьютеры будут понимать только компьютеры на базе Intel.

Ответ 4

Замените функцию firstBit внутренним с помощью команды BSF или BSR для массового ускорения.

В gcc это будет __builtin_ffsll и __builtin_ctzll

С Visual C +, _BitScanForward и _BitScanReverse

Ответ 5

Самый быстрый, о котором я могу думать сейчас, будет использовать предварительно сгенерированный массив map всех чисел (он не обязательно должен быть всеми числами, вы можете, например, разбить числа в 8-битных или 16-битных частях, а затем объединить возвращенные массивы с некоторыми правильными дополнениями для учета фактического положения бит).

Ответ 6

const size_t size = sizeof(U64)*8;
U64 val = 1;

vector<UINT> res;
res.reserve(size);

for ( size_t i = size; i > 0; --i ) {
  if ( ( val & bitboard ) != 0 ) {
    res.push_back(i);
  }
  val <<= 1;
}

Ответ 7

Я попробовал наивную версию, которая работала примерно в 2-3 раза быстрее, но сначала спрятала() 'd вектор. При применении резерва к исходному алгоритму он избил наивный.

Итак, я подозреваю, что векторные операции здесь являются большими затратами, а не манипуляциями с битами (или даже умножением, используемым в следующей бит-функции).

Есть еще несколько ускорений, связанных с поиском младшего бита. Моя локальная версия log2 была плохой, и функция, указанная в исходном сообщении, не была очень дешевой.

Вот моя лучшая попытка:

void bits(uint64 bitboard, vector<int> &res)
{
    res.resize(64);
    int i = 0;
    while (bitboard)
    {
        int first;
        _BitScanForward64((DWORD *)&first, bitboard);
        res[i++] = first;
        bitboard &= ~(1ULL<<first);
    }
    res.resize(i);
}

Заменил функцию firstBit встроенным asm. Использование внутреннего эффекта дало большой толчок. (Это, очевидно, не переносимый код, хотя я подозреваю, что вариант GCC не должен быть слишком сложным).

Также предполагается, что вектор достаточно устойчив, а не динамически выделяется/копируется все время и просто изменяет его соответствующим образом.

Ответ 8

Я на самом деле считаю, что самый быстрый и простой метод - это просто зацикливаться, но если мы передадим вектор вместо последующей копии, он должен быть немного быстрее.

void DQBitboard::bits(U64 bitboard, vector<UINT> &res)
{
    res.clear();   // Make sure vector is empty. Not necessary if caller does this!
    int bit = 0;
    while (bitboard)
    {
        if (bitboard & 1) 
            res.push_back(bit);
        bit++;
        bitboard >>= 1;
    }

    return res;
}

Умножение в findfirst будет стоить немного, поэтому я сомневаюсь, что это действительно того стоит.