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

Каков самый быстрый способ вернуть позиции всех заданных битов в 64-битное целое число?

Мне нужен быстрый способ получить позицию всех одного бита в 64-битном целое. Например, учитывая x = 123703, я хотел бы заполнить массив idx[] = {0, 1, 2, 4, 5, 8, 9, 13, 14, 15, 16}. Мы можем предположить, что мы знаем число бит априори. Это будет называться 10 ^ 12 - 10 ^ 15 раз, поэтому скорость имеет смысл. Самый быстрый ответ, который я получил до сих пор, - это следующий монстр, который использует каждый бит 64-битного целого в качестве индекса в таблицах, которые дают количество бит, установленных в этом байте, и их позиции:

int64_t x;            // this is the input
unsigned char idx[K]; // this is the array of K bits that are set
unsigned char *dst=idx, *src;
unsigned char zero, one, two, three, four, five;  // these hold the 0th-5th bytes
zero  =  x & 0x0000000000FFUL;
one   = (x & 0x00000000FF00UL) >> 8;
two   = (x & 0x000000FF0000UL) >> 16;
three = (x & 0x0000FF000000UL) >> 24;
four  = (x & 0x00FF00000000UL) >> 32;
five  = (x & 0xFF0000000000UL) >> 40;
src=tab0+tabofs[zero ]; COPY(dst, src, n[zero ]);
src=tab1+tabofs[one  ]; COPY(dst, src, n[one  ]);
src=tab2+tabofs[two  ]; COPY(dst, src, n[two  ]);
src=tab3+tabofs[three]; COPY(dst, src, n[three]);
src=tab4+tabofs[four ]; COPY(dst, src, n[four ]);
src=tab5+tabofs[five ]; COPY(dst, src, n[five ]);

где COPY - оператор switch для копирования до 8 байтов, n - это массив числа бит, заданного в байте, и tabofs дает смещение в tabX, которое удерживает позиции установить бит в X-й байт. Это примерно в 3 раза быстрее, чем разворачиваемые методы на основе цикла с помощью __builtin_ctz() на моем Xeon E5-2609. (см. ниже). Я в настоящее время повторяю x в лексикографическом порядке для заданного количества бит набор.

Есть ли лучший способ?

EDIT: добавлен пример (который я впоследствии исправил). Полный код доступен здесь: http://pastebin.com/79X8XL2P. Примечание: GCC с -O2, похоже, оптимизирует его, но компилятор Intel (который я использовал для его создания) не...

Кроме того, позвольте мне дать дополнительную информацию, чтобы рассмотреть некоторые из комментариев ниже. Цель состоит в том, чтобы выполнить статистический тест на каждом возможном подмножестве K переменных из универсума N возможных объясняющих переменных; конкретная цель прямо сейчас равна N = 41, но я вижу некоторые проекты, требующие N до 45-50. Тест в основном включает факторизацию соответствующей подматрицы данных. В псевдокоде что-то вроде этого:

double doTest(double *data, int64_t model) {
  int nidx, idx[];
  double submatrix[][];
  nidx = getIndices(model, idx);  // get the locations of ones in model
  // copy data into submatrix
  for(int i=0; i<nidx; i++) {
    for(int j=0; j<nidx; j++) {
      submatrix[i][j] = data[idx[i]][idx[j]];
    }
  }
  factorize(submatrix, nidx);
  return the_answer;
}

Я закодировал версию этого для платы Intel Phi, которая должна завершить N = 41 случай примерно через 15 дней, из которых ~ 5-10% времени тратится на наивный getIndices(), поэтому сразу после bat более быстрая версия могла бы сэкономить день или больше. Я тоже работаю над реализацией для NVidia Kepler, но, к сожалению, проблема, которую я имею (смехотворные числа операций с малой матрицей), не идеально подходит для аппаратного обеспечения (смехотворно большие матричные операции). Тем не менее, этот документ представляет собой решение, которое, по-видимому, достигает сотен GFLOPS/s на матрицах моего размера, агрессивно разворачивая петли и выполняя всю факторизацию в регистрах, что размеры матрицы определяются во время компиляции. (Это разворачивание цикла должно помочь уменьшить накладные расходы и улучшить векторизация в версии Phi, поэтому getIndices() станет более важным!) Итак, теперь я думаю, что мое ядро ​​должно выглядеть больше:

double *data;  // move data to GPU/Phi once into shared memory
template<unsigned int K> double doTestUnrolled(int *idx) {
  double submatrix[K][K];
  // copy data into submatrix
  #pragma unroll
  for(int i=0; i<K; i++) {
    #pragma unroll
    for(int j=0; j<K; j++) {
      submatrix[i][j] = data[idx[i]][idx[j]];
    }
  }
  factorizeUnrolled<K>(submatrix);
  return the_answer;
}

Версия Phi решает каждую модель в цикле `cilk_for 'от модели = 0 до 2 ^ N (или, скорее, подмножество для тестирования), но теперь для пакетной работы для графического процессора и амортизации начальных издержек запуска ядра Я должен повторять номера моделей в лексикографическом порядке для каждого из K = от 1 до 41 бит (как отмечал doynax).

EDIT 2: Теперь, когда отпуск закончился, вот некоторые результаты на моем Xeon E5-2602 с помощью icc версии 15. Код, который я использовал для сравнения: http://pastebin.com/XvrGQUat. Я выполняю извлечение бит для целых чисел, у которых задано ровно K бит, поэтому есть некоторые накладные расходы для лексикографической итерации, измеренные в столбце "База" в таблице ниже. Они выполняются 2 ^ 30 раз с N = 48 (повторяя при необходимости).

"CTZ" - это цикл, который использует gcc intrinsic __builtin_ctzll для получения бит младшего разряда:

for(int i=0; i<K; i++) {
    idx[i] = __builtin_ctzll(tmp);
    lb = tmp & -tmp;    // get lowest bit
    tmp ^= lb;      // remove lowest bit from tmp
} 

Отметка Mark не привязана к циклу:

for(int i=0; i<K; i++) {
    *dst = i;
    dst += x & 1;
    x >>= 1;
} 

Tab1 - мой исходный код на основе таблиц со следующим макросом копирования:

#define COPY(d, s, n) \
switch(n) { \
case 8: *(d++) = *(s++); \
case 7: *(d++) = *(s++); \
case 6: *(d++) = *(s++); \
case 5: *(d++) = *(s++); \
case 4: *(d++) = *(s++); \
case 3: *(d++) = *(s++); \
case 2: *(d++) = *(s++); \
case 1: *(d++) = *(s++); \
case 0: break;        \
}

Tab2 - это тот же код, что и Tab1, но макрос копирования просто перемещает 8 байтов в виде одной копии (принимая идеи от doynax и Lưu Vĩnh Phúc... но обратите внимание, что это не обеспечивает выравнивание):

#define COPY2(d, s, n) { *((uint64_t *)d) = *((uint64_t *)s); d+=n; }

Вот результаты. Я предполагаю, что мое первоначальное утверждение о том, что Tab1 в 3 раза быстрее, чем CTZ, выполняется только для больших K (где я тестировал). Марк цикл быстрее, чем мой исходный код, но избавление от ветки в макросе COPY2 принимает торт для K > 8.

 K    Base    CTZ   Mark   Tab1   Tab2
001  4.97s  6.42s  6.66s 18.23s 12.77s
002  4.95s  8.49s  7.28s 19.50s 12.33s
004  4.95s  9.83s  8.68s 19.74s 11.92s
006  4.95s 16.86s  9.53s 20.48s 11.66s
008  4.95s 19.21s 13.87s 20.77s 11.92s
010  4.95s 21.53s 13.09s 21.02s 11.28s
015  4.95s 32.64s 17.75s 23.30s 10.98s
020  4.99s 42.00s 21.75s 27.15s 10.96s
030  5.00s 100.64s 35.48s 35.84s 11.07s
040  5.01s 131.96s 44.55s 44.51s 11.58s
4b9b3361

Ответ 1

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

Судя по вашему образцу кода и предыдущему запросу SO, вы перечисляете все слова с установленными по порядку битами K и извлекаете из них битовые индексы. Это значительно упрощает дело.

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

Что-то в этом роде:

// Walk through all len-bit words with num-bits set in order
void enumerate(size_t num, size_t len) {
    size_t i;
    unsigned int bitpos[64 + 1];

    // Seed with the lowest word plus a sentinel
    for(i = 0; i < num; ++i)
        bitpos[i] = i;
    bitpos[i] = 0;

    // Here goes the main loop
    do {
        // Do something with the resulting data
        process(bitpos, num);

        // Increment the least-significant series of consecutive bits
        for(i = 0; bitpos[i + 1] == bitpos[i] + 1; ++i)
            bitpos[i] = i;
    // Stop on reaching the top
    } while(++bitpos[i] != len);
}

// Test function
void process(const unsigned int *bits, size_t num) {
    do
        printf("%d ", bits[--num]);
    while(num);
    putchar('\n');
}

Не особенно оптимизирован, но вы получаете общую идею.

Ответ 2

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

unsigned char idx[K+1]; // need one extra for overwrite protection
unsigned char *dst=idx;
for (unsigned char i = 0; i < 50; i++)
{
    *dst = i;
    dst += x & 1;
    x >>= 1;
}

P.S. ваш образец вывода в вопросе неверен, см. http://ideone.com/2o032E

Ответ 3

Как минимальная модификация:

int64_t x;            
char idx[K+1];
char *dst=idx;
const int BITS = 8;
for (int i = 0 ; i < 64+BITS; i += BITS) {
  int y = (x & ((1<<BITS)-1));
  char* end = strcat(dst, tab[y]); // tab[y] is a _string_
  for (; dst != end; ++dst)
  {
    *dst += (i - 1); // tab[] is null-terminated so bit positions are 1 to BITS.
  }
  x >>= BITS;
}

Выбор BITS определяет размер таблицы. 8, 13 и 16 являются логическим выбором. Каждая запись представляет собой строку с нулевым завершением и содержит позиции бит с 1 смещением. То есть вкладка [5] равна "\x03\x01". Внутренний цикл фиксирует это смещение.

Чуть более эффективный: замените strcat и внутренний цикл на

char const* ptr = tab[y];
while (*ptr)
{
   *dst++ = *ptr++ + (i-1);
}

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

Одна вещь, которую я рассматриваю, заключается в том, что tab[y] представляет собой массив указателей на строки. Они очень похожи: "\x1" является суффиксом "\x3\x1". Фактически, каждая строка, которая не начинается с "\x8", является суффиксом строки, которая делает. Мне интересно, сколько уникальных строк вам нужно, и в какой степени tab[y] на самом деле требуется. Например. по логике выше, tab[128+x] == tab[x]-1.

[править]

Nevermind, вам определенно требуется 128 вкладок, начиная с "\x8", так как они никогда не являются суффиксом другой строки. Тем не менее, правило tab[128+x] == tab[x]-1 означает, что вы можете сохранить половину записей, но за счет двух дополнительных инструкций: char const* ptr = tab[x & 0x7F] - ((x>>7) & 1). (Настройте tab[] на точку после \x8)

Ответ 4

Использование char не поможет вам увеличить скорость, но на самом деле часто требуется больше ANDing и sign/zero, распространяющихся при вычислении. Только в случае очень больших массивов, которые должны входить в кеш, следует использовать меньшие типы int

Еще одна вещь, которую вы можете улучшить, - макрос COPY. Вместо того, чтобы копировать побайтовые байты, скопируйте все слова, если возможно

inline COPY(unsigned char *dst, unsigned char *src, int n)
{
switch(n) { // remember to align dst and src when declaring
case 8:
    *((int64_t*)dst) = *((int64_t*)src);
    break;
case 7:
    *((int32_t*)dst) = *((int32_t*)src);
    *((int16_t*)(dst + 4)) = *((int32_t*)(src + 4));
    dst[6] = src[6];
    break;
case 6:
    *((int32_t*)dst) = *((int32_t*)src);
    *((int16_t*)(dst + 4)) = *((int32_t*)(src + 4));
    break;
case 5:
    *((int32_t*)dst) = *((int32_t*)src);
    dst[4] = src[4];
    break;
case 4:
    *((int32_t*)dst) = *((int32_t*)src);
    break;
case 3:
    *((int16_t*)dst) = *((int16_t*)src);
    dst[2] = src[2];
    break;
case 2:
    *((int16_t*)dst) = *((int16_t*)src);
    break;
case 1:
    dst[0] = src[0];
    break;
case 0:
    break;
}

Кроме того, поскольку tabofs [x] и n [x] часто имеют доступ друг к другу, попробуйте поместить его в память, чтобы убедиться, что они всегда находятся в кеше в то же время

typedef struct TAB_N
{
    int16_t n, tabofs;
} tab_n[256];

src=tab0+tab_n[b0].tabofs; COPY(dst, src, tab_n[b0].n);
src=tab0+tab_n[b1].tabofs; COPY(dst, src, tab_n[b1].n);
src=tab0+tab_n[b2].tabofs; COPY(dst, src, tab_n[b2].n);
src=tab0+tab_n[b3].tabofs; COPY(dst, src, tab_n[b3].n);
src=tab0+tab_n[b4].tabofs; COPY(dst, src, tab_n[b4].n);
src=tab0+tab_n[b5].tabofs; COPY(dst, src, tab_n[b5].n);

И последнее, но не менее важное: gettimeofday не для подсчета производительности. Используйте QueryPerformanceCounter, гораздо точнее

Ответ 5

В вашем коде используется индексная таблица с 1 байтом (256 записей). Вы можете ускорить его в 2 раза, если вы используете таблицу с 2 байтами (65536 записей).

К сожалению, вы, вероятно, не можете продлить это в дальнейшем - для 3-байтного размера таблицы будет 16 МБ, вряд ли вместиться в локальный кеш процессора, и это будет только замедлять работу.

Ответ 6

Вопрос в том, что вы собираетесь делать с коллекцией позиций?
Если вам придется много раз повторять это, тогда да, может быть интересно собрать их один раз, как вы делаете сейчас, и повторить многие.
Но если он для итерации один или несколько раз, то вы можете подумать о том, чтобы не создавать промежуточный массив позиций, и просто вызывать блокировку/функцию блока обработки при каждом обнаружении 1 при повторении битов.

Вот наивный пример битового итератора, который я написал в Smalltalk:

LargePositiveInteger>>bitsDo: aBlock
| mask offset |
1 to: self digitLength do: [:iByte |
    offset := (iByte - 1) << 3.
    mask := (self digitAt: iByte).
    [mask = 0]
        whileFalse:
            [aBlock value: mask lowBit + offset.
            mask := mask bitAnd: mask - 1]]

A LargePositiveInteger - целое число произвольной длины, состоящее из цифр байта. LowBit отвечает на ранжирование младшего разряда и реализуется как таблица поиска с 256 элементами.

В С++ 2011 вы можете легко пройти закрытие, поэтому его нужно легко перевести.

uint64_t x;
unsigned int mask;
void (*process_bit_position)(unsigned int);
unsigned char offset = 0;
unsigned char lowBitTable[16] = {0,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0}; // 0-based, first entry is unused
while( x )
{
    mask = x & 0xFUL;
    while (mask)
    {
        process_bit_position( lowBitTable[mask]+offset );
        mask &= mask - 1;
    }
    offset += 4;
    x >>= 4;
}

Пример демонстрируется с 4-битной таблицей, но вы можете легко расширить его до 13 бит или более, если он подходит в кеш.

Для предсказания ветвлений внутренний цикл можно переписать как for(i=0;i<nbit;i++) с дополнительной таблицей nbit=numBitTable[mask], а затем развернут с помощью переключателя (компилятор мог бы это сделать?), но я даю вам возможность оценить, как он работает в первую очередь..

Ответ 7

Было ли это слишком медленным?
Мало и грубо, но все это в кэше и регистры процессора;

void mybits(uint64_t x, unsigned char *idx)
{
  unsigned char n = 0;
  do {
    if (x & 1) *(idx++) = n;
    n++;
  } while (x >>= 1);          // If x is signed this will never end
  *idx = (unsigned char) 255; // List Terminator
}

Это еще 3 раза быстрее, чтобы развернуть цикл и создать массив из 64 значений true/false (что совсем не так)

void mybits_3_2(uint64_t x, idx_type idx[])
{
#define SET(i) (idx[i] = (x & (1UL<<i)))
  SET( 0);
  SET( 1);
  SET( 2);
  SET( 3);
  ...
  SET(63);
}

Ответ 8

Вот некоторый жесткий код, написанный для 1-байтового (8 бит), но он должен легко, очевидно, расширяться до 64 бит.

int main(void)
{
    int x = 187;

    int ans[8] = {-1,-1,-1,-1,-1,-1,-1,-1};
    int idx = 0;

    while (x)
    {
        switch (x & ~(x-1))
        {
        case 0x01: ans[idx++] = 0; break;
        case 0x02: ans[idx++] = 1; break;
        case 0x04: ans[idx++] = 2; break;
        case 0x08: ans[idx++] = 3; break;
        case 0x10: ans[idx++] = 4; break;
        case 0x20: ans[idx++] = 5; break;
        case 0x40: ans[idx++] = 6; break;
        case 0x80: ans[idx++] = 7; break;
        }

        x &= x-1;
    }

   getchar();
   return 0;
}

Выходной массив должен быть:

ans = {0,1,3,4,5,7,-1,-1};

Ответ 9

Если я беру "мне нужен быстрый способ получить позицию всех одного бита в 64-битном целое" буквально...

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

например. для арифметического сдвига влево (проверка с бита 64 на бит 0)....

pseudo code (ignore instruction mix etc errors and oversimplification...been a while):

    move #64+1, counter
    loop. ASL 64bitinteger       
    BCS carryset
    decctr. dec counter
    bne loop
    exit

    carryset. 
    //store #counter-1 (i.e. bit position) in datastruct indexed by counter
    jmp decctr

... Надеюсь, у вас есть идея.

Я не использовал сборку с тех пор, но мне интересно, можем ли мы использовать встроенную сборку С++, подобную приведенной выше, чтобы сделать что-то подобное здесь. Мы могли бы сделать всю конверсию в сборке (очень мало строк кода), создав соответствующую структуру данных. С++ мог просто проверить ответ.

Если это возможно, я бы предположил, что это довольно быстро.

Ответ 10

Предполагая разреженность в количестве заданных битов,

int count = 0;
unsigned int tmp_bitmap = x;        
while (tmp_bitmap > 0) {
    int next_psn = __builtin_ffs(tmp_bitmap) - 1;
    tmp_bitmap &= (tmp_bitmap-1);
    id[count++] = next_psn;
}