Подсчет битов в непрерывном блоке памяти - PullRequest
16 голосов
/ 27 августа 2011

Мне задали в интервью следующий вопрос.

int countSetBits(void *ptr, int start, int end); 

Справка: Предположим, что ptr указывает на большой кусок памяти. Рассматривая эту память как непрерывную последовательность битов, start и end являются позициями битов. Предположим, start и end имеют правильные значения, а ptr указывает на инициализированный кусок памяти.

Вопрос: Напишите код C для подсчета количества битов, установленных от start до end [включительно], и верните счет.

Просто чтобы было понятнее

 ptr---->+-------------------------------+
         | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
         +-------------------------------+
         | 8 | 9 |                   |15 |
         +-------------------------------+
         |                               |
         +-------------------------------+
              ...
              ...
         +-------------------------------+
         |               | S |           |
         +-------------------------------+
              ...
              ...
         +-------------------------------+
         |    | E |                      |
         +-------------------------------+
              ...
              ...

Мое решение:

int countSetBits(void *ptr, int start, int end )
{
    int count = 0, idx; 

    char *ch; 

    for (idx = start; idx <= end; idx++) 
    {     ch = ptr + (idx/8); 

          if((128 >> (idx%8)) & (*ch)) 
          {
                   count++; 
          }
    }

    return count; 
}

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

Я очень уверен, что SO сообщество может предоставить более элегантное решение. Мне просто любопытно увидеть их ответ.

PS: вышеуказанный код не скомпилирован. Это больше похоже на псевдокод и может содержать ошибки.

Ответы [ 8 ]

10 голосов
/ 27 августа 2011

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

как то так:

int bit_table[256] = {0, 1, 1, 2, 1, ...};
char* p = ptr + start;
int count = 0;
for (p; p != ptr + end; p++)
    count += bit_table[*(unsigned char*)p];
9 голосов
/ 27 августа 2011

Граничные условия, они не получают уважения ...

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

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

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

Вот решение, которое использует справочную таблицу и пытается по-прежнему обрабатывать границы (это только слегка проверено, поэтому я не буду утверждать, что это на 100% правильно). Это также уродливее, чем хотелось бы, но уже поздно:

typedef unsigned char uint8_t;

static
size_t bits_in_byte( uint8_t val)
{
    static int const half_byte[] = { 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4 };

    int result1 = half_byte[val & 0x0f];
    int result2 = half_byte[(val >> 4) & 0x0f];

    return result1 + result2;
}


int countSetBits( void* ptr, int start, int end) 
{
    uint8_t*    first;
    uint8_t*    last;
    int         bits_first;
    int         bits_last;
    uint8_t     mask_first;
    uint8_t     mask_last;

    size_t count = 0;

    // get bits from the first byte
    first = ((uint8_t*) ptr) + (start / 8);
    bits_first = 8 - start % 8;
    mask_first = (1 << bits_first) - 1;
    mask_first = mask_first << (8 - bits_first);


    // get bits from last byte
    last = ((uint8_t*) ptr) + (end / 8);
    bits_last = 1 + (end % 8);
    mask_last = (1 << bits_last) - 1;

    if (first == last) {
        // we only have a range of bits in  the first byte
        count = bits_in_byte( (*first) & mask_first & mask_last);        
    }
    else {
        // handle the bits from the first and last bytes specially
        count += bits_in_byte((*first) & mask_first);
        count += bits_in_byte((*last) & mask_last);

        // now we've collected the odds and ends from the start and end of the bit range
        // handle the full bytes in the interior of the range

        for (first = first+1; first != last; ++first) {
            count += bits_in_byte(*first);
        }
    }

    return count;
}

Обратите внимание, что деталь, которая должна быть проработана как часть интервью, заключается в том, индексируются ли биты внутри байта, начиная с младшего значащего бита (lsb) или старшего значащего бита (мсб). Другими словами, если начальный индекс был задан как 0, будет ли байт со значением 0x01 или байт со значением 0x80 иметь бит, установленный в этом индексе? Вроде того, как решить, считают ли индексы порядок следования битов в байте как байты с прямым порядком байтов или с прямым порядком байтов.

Нет «правильного» ответа на это - интервьюер должен был бы указать, каким должно быть поведение. Я также отмечу, что мое примерное решение обрабатывает это противоположно коду примера OP (я шел по тому, как я интерпретировал диаграмму, при этом индексы также читались как «битовые числа»). Решение OP рассматривает битовый порядок как big-endian, моя функция обрабатывает их как little-endian. Таким образом, хотя оба обрабатывают частичные байты в начале и в конце диапазона, они дают разные ответы. Какой правильный ответ зависит от того, какова реальная спецификация для проблемы.

4 голосов
/ 27 августа 2011

Версия @dimitri, вероятно, самая быстрая. Но трудно составить таблицу подсчета бит для всех 128 8-битных символов в интервью. Вы можете получить очень быструю версию с таблицей для 16 шестнадцатеричных чисел 0x0, 0x1, ..., 0xF, которую вы можете легко построить:

int countBits(void *ptr, int start, int end) {
    // start, end are byte indexes
    int hexCounts[16] =   {0, 1, 1, 2,   1, 2, 2, 3,
                           1, 2, 3, 3,   2, 3, 3, 4}; 
    unsigned char * pstart = (unsigned char *) ptr + start;
    unsigned char * pend = (unsigned char *) ptr + end;
    int count = 0;
    for (unsigned char * p = pstart; p <= pend; ++p) {
        unsigned char b = *p;
        count += hexCounts[b & 0x0F] + hexCounts[(b >> 4) & 0x0F];
    }
    return count;
}

РЕДАКТИРОВАТЬ: Если start и end являются битовыми индексами, то биты в первом и последнем байтах будут считаться первыми перед вызовом вышеупомянутой функции:

int countBits2(void *ptr, int start, int end) {
    // start, end are bit indexes
    if (start > end) return 0;
    int count = 0;
    unsigned char* pstart = (unsigned char *) ptr + start/8; // first byte
    unsigned char* pend = (unsigned char *) ptr + end/8;     // last byte
    int istart = start % 8;                                  // index in first byte
    int iend = end % 8;                                      // index in last byte 
    unsigned char b = *pstart;                               // byte
    if (pstart == pend) {                                    // count in 1 byte only
        b = b << istart;
        for (int i = istart; i <= iend; ++i) {               // between istart, iend
            if (b & 0x80) ++count; 
            b = b << 1;
        }
    }
    else {                                                   // count in 2 bytes
        for (int i = istart; i < 8; ++i) {                   // from istart to 7
            if (b & 1) ++count; 
            b = b >> 1;
        }
        b = *pend;
        for (int i = 0; i <= iend; ++i) {                    // from 0 to iend
            if (b & 0x80) ++count; 
            b = b << 1;
        }
    }
    return count + countBits(ptr, start/8 + 1, end/8 - 1);
}
4 голосов
/ 27 августа 2011

Вы можете найти эту страницу интересной, она содержит несколько альтернативных решений для вашей проблемы.

3 голосов
/ 27 августа 2011

Есть множество способов решения проблемы. Этот - хороший пост, в котором сравнивается производительность наиболее распространенных вариантов.

1 голос
/ 12 апреля 2018

Отличное недавнее исследование, в котором сравниваются некоторые из самых современных методов подсчета числа «установленных» (1-значных) битов в диапазоне памяти ( aka Вес Хэмминга , мощность в битах , Боковая сумма , Количество населения или popcnt, и т. Д. ) можно найти в Wojciech, Kurz и Lemire (2017), Более быстрый подсчет населения с использованием инструкций AVX2 1

Ниже приведена полная, протестированная и полностью работающая C # адаптация алгоритма "Harley-Seal" из этой статьи, которую авторы считают самым быстрым методомкоторый использует побитовые операции общего назначения (то есть, не требующие специального оборудования).

1.Точки входа в управляемый массив
(необязательно) Предоставляет доступ к оптимизированному для блоков счету битов для управляемого массива ulong[].

/// <summary> Returns the total number of 1-valued bits in the array </summary>
[DebuggerStepThrough]
public static int OnesCount(ulong[] rg) => OnesCount(rg, 0, rg.Length);

/// <summary> Finds the total number of '1' bits in an array or its subset </summary>
/// <param name="rg"> Array of ulong values to scan </param>
/// <param name="index"> Starting index in the array </param>
/// <param name="count"> Number of ulong values to examine, starting at 'i' </param>
public static int OnesCount(ulong[] rg, int index, int count)
{
    if ((index | count) < 0 || index > rg.Length - count)
        throw new ArgumentException();

    fixed (ulong* p = &rg[index])
        return OnesCount(p, count);
}

2.Скалярное API
Используется счетчиком, оптимизированным для блоков, для агрегирования результатов сумматора с сохранением переноса, а также для завершения любого остатка для размеров блоков, не делимых на оптимизированный размер фрагмента 16 x 8 байт / ulong =128 байтов.Также подходит для общего применения.

/// <summary> Finds the Hamming Weight or ones-count of a ulong value </summary>
/// <returns> The number of 1-bits that are set in 'x' </returns>
public static int OnesCount(ulong x)
{
    x -= (x >> 1) & 0x5555555555555555;
    x = ((x >> 2) & 0x3333333333333333) + (x & 0x3333333333333333);
    return (int)((((x + (x >> 4)) & 0x0F0F0F0F0F0F0F0F) * 0x0101010101010101) >> 56);
}

3. "Harley-Seal" 1-битный счетчик, оптимизированный для блоков *
Обрабатывает блоки по 128 байт за раз, то есть 16 ulong значений на блок.Использует сумматор с сохранением переноса (показанный ниже) для группового сложения единичных битов между соседними ulong с и суммирует итоговые значения в виде степеней, равных двум.Сумматор с переносом (CSA)

/// <summary> carry-save adder </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static ulong CSA(ref ulong a, ulong b, ulong c)
{
    ulong v = a & b | (a ^ b) & c;
    a ^= b ^ c;
    return v;
}


Примечания

Поскольку подход, показанный здесь, подсчитывает общее количество 1-бит, продолжая128-байтовые блоки за раз, это становится оптимальным только при больших размерах блоков памяти.Например, вероятно, по крайней мере, некоторое (маленькое) кратное размеру этого фрагмента из шестнадцати слов (16- ulong).Для подсчета 1 бита в меньших диапазонах памяти этот код будет работать правильно, но значительно хуже, чем более наивные методы.Подробности см. В статье.

На этой диаграмме показано, как работает сумматор для переноски :

Carry-Save Adder in 'Harley-Seal' block-optimized bit count


Ссылки

[1.] Мула, Войцех, Натан Курц и Даниэль Лемир.«Более быстрое население учитывает инструкции AVX2».Компьютерный журнал 61, нет.1 (2017): 111-120.

0 голосов
/ 29 августа 2011

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

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

int countSetBits(void *ptr, int start, int end )
{
    assert(start < end);

    unsigned char *s = ((unsigned char*)ptr + start);
    unsigned char *e = ((unsigned char*)ptr + end);

    int r = 0;

    while(s != e)
    {
        // __builtin_clz is not defined for 0 input.
        if(*s) r += 32 - __builtin_clz(*s);
        s++;
    }

    return r;
}
0 голосов
/ 27 августа 2011

Отказ от ответственности: не предпринимались попытки скомпилировать следующий код.

/*
 * Table counting the number of set bits in a byte.
 * The byte is the index to the table.
 */
uint8_t  table[256] = {...};

/***************************************************************************
 *
 * countBits - count the number of set bits in a range
 *
 * The most significant bit in the byte is considered to be bit 0.
 *
 * RETURNS: 0 on success, -1 on failure
 */
int countBits (
    uint8_t *  buffer,
    int        startBit,  /* starting bit */
    int        endBit,    /* End-bit (inlcusive) */
    unsigned * pTotal     /* Output: number of consecutively set bits */
    ) {
    int      numBits;     /* number of bits left to check */
    int      mask;        /* mask to apply to byte from <buffer> */
    int      bits;        /* # of bits to end of byte */
    unsigned count = 0;   /* total number of bits set */
    uint8_t  value;       /* value read from the buffer */

    /* Return -1 if parameters fail sanity check (skipped) */

    numBits   = (endBit - startBit) + 1;

    index  = startBit >> 3;
    bits   = 8 - (startBit & 7);
    mask   = (1 << bits) - 1;

    value = buffer[index] & mask;  /* mask-out any bits preceding <startBit> */
    numBits -= bits;

    while (numBits > 0) {          /* Note: if <startBit> and <endBit> are in */
        count += table[value];     /* same byte, this loop gets skipped. */
        index++;
        value = buffer[index];
        numBits -= 8;
    }

    if (numBits < 0) {             /* mask-out any bits following <endBit> */
        bits   = 8 - (endBit & 7);
        mask   = 0xff << bits;
        value &= mask;
    }

    count += table[value];

    *pTotal = count;
    return 0;
}

Редактировать: заголовок функции обновлен.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...