Битовая упаковка массива целых чисел

У меня есть массив целых чисел, давайте предположим, что они имеют тип int64_t, Теперь я знаю, что только каждый первый n биты каждого целого числа имеют смысл (то есть я знаю, что они ограничены некоторыми границами).

Какой самый эффективный способ преобразовать массив таким образом, чтобы удалить все ненужное пространство (т.е. у меня есть первое целое число в a[0]второй на a[0] + n bits и так далее)?

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

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

Редактировать:

Этот вопрос не о сжатии массива, чтобы занять как можно меньше места. Мне просто нужно "срезать" n bits из каждого целого числа и учитывая массив я знаю точное n бит, которые я могу безопасно вырезать.

7 ответов

Сегодня я выпустил: PackedArray: Упаковка целых чисел без знака ( проект на github).

Он реализует контейнер произвольного доступа, где элементы упакованы на уровне битов. Другими словами, он действует так, как если бы вы были в состоянии манипулировать, например, uint9_t или же uint17_t массив:

PackedArray principle:
  . compact storage of <= 32 bits items
  . items are tightly packed into a buffer of uint32_t integers

PackedArray requirements:
  . you must know in advance how many bits are needed to hold a single item
  . you must know in advance how many items you want to store
  . when packing, behavior is undefined if items have more than bitsPerItem bits

PackedArray general in memory representation:
  |-------------------------------------------------- - - -
  |       b0       |       b1       |       b2       |
  |-------------------------------------------------- - - -
  | i0 | i1 | i2 | i3 | i4 | i5 | i6 | i7 | i8 | i9 |
  |-------------------------------------------------- - - -

  . items are tightly packed together
  . several items end up inside the same buffer cell, e.g. i0, i1, i2
  . some items span two buffer cells, e.g. i3, i6

Я согласен с Керабой, что вам нужно использовать что-то вроде кодирования Хаффмана или, возможно, алгоритм Лемпеля-Зива-Уэлча. Проблема с упаковкой битов, о которой вы говорите, заключается в том, что у вас есть два варианта:

  • Выберите постоянную n такую, чтобы можно было представить наибольшее целое число.
  • Позвольте n меняться от значения к значению.

Первый вариант относительно прост в реализации, но он действительно будет тратить много места, если все целые числа не достаточно малы.

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

Преимущество Хаффмана или LZW состоит в том, что они создают кодовые книги таким образом, что длина кодов может быть получена из выходного потока битов без фактического сохранения длин. Эти методы позволяют вам приблизиться к пределу Шеннона.

Я решил попробовать оригинальную идею (константа n, удалить неиспользуемые биты и упаковать), и вот наивная реализация, которую я придумал:

#include <sys/types.h>
#include <stdio.h>

int pack(int64_t* input, int nin, void* output, int n)
{
    int64_t inmask = 0;
    unsigned char* pout = (unsigned char*)output;
    int obit = 0;
    int nout = 0;
    *pout = 0;

    for(int i=0; i<nin; i++)
    {
        inmask = (int64_t)1 << (n-1);
        for(int k=0; k<n; k++)
        {
            if(obit>7)
            {
                obit = 0;
                pout++;
                *pout = 0;
            }
            *pout |= (((input[i] & inmask) >> (n-k-1)) << (7-obit));
            inmask >>= 1;
            obit++;
            nout++;
        }
    }
    return nout;
}

int unpack(void* input, int nbitsin, int64_t* output, int n)
{
    unsigned char* pin = (unsigned char*)input;
    int64_t* pout = output;
    int nbits = nbitsin;
    unsigned char inmask = 0x80;
    int inbit = 0;
    int nout = 0;
    while(nbits > 0)
    {
        *pout = 0;
        for(int i=0; i<n; i++)
        {
            if(inbit > 7)
            {
                pin++;
                inbit = 0;
            }
            *pout |= ((int64_t)((*pin & (inmask >> inbit)) >> (7-inbit))) << (n-i-1);
            inbit++;
        }
        pout++;
        nbits -= n;
        nout++;
    }
    return nout;
}

int main()
{
    int64_t input[] = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20};
    int64_t output[21];
    unsigned char compressed[21*8];
    int n = 5;

    int nbits = pack(input, 21, compressed, n);
    int nout = unpack(compressed, nbits, output, n);

    for(int i=0; i<=20; i++)
        printf("input: %lld   output: %lld\n", input[i], output[i]);
}

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

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

Начиная с реализации Джейсона Б., в конце концов я написал свою собственную версию, которая обрабатывает битовые блоки вместо отдельных битов. Единственное отличие состоит в том, что это lsb: он начинается с младших битов, идущих к старшим Это только затрудняет чтение с бинарным дампом, как в Linux xxd -b, Как деталь, int* можно тривиально изменить на int64_t*и это должно быть еще лучше unsigned, Я уже тестировал эту версию с несколькими миллионами массивов, и она кажется надежной, поэтому я поделюсь с остальными:

int pack2(int *input, int nin, unsigned char* output, int n)
{
        int obit = 0;
        int ibit = 0;
        int ibite = 0;
        int nout = 0;
        if(nin>0) output[0] = 0;
        for(int i=0; i<nin; i++)
        {
                ibit = 0;
                while(ibit < n) {
                        ibite = std::min(n, ibit + 8 - obit);
                        output[nout] |= (input[i] & (((1 << ibite)-1) ^ ((1 << ibit)-1))) >> ibit << obit;
                        obit += ibite - ibit;
                        nout += obit >> 3;
                        if(obit & 8) output[nout] = 0;
                        obit &= 7;
                        ibit = ibite;
                }
        }
        return nout;
}

int unpack2(int *oinput, int nin, unsigned char* ioutput, int n)
{
        int obit = 0;
        int ibit = 0;
        int ibite = 0;
        int nout = 0;
        for(int i=0; i<nin; i++)
        {
                oinput[i] = 0;
                ibit = 0;
                while(ibit < n) {
                        ibite = std::min(n, ibit + 8 - obit);
                        oinput[i] |= (ioutput[nout] & (((1 << (ibite-ibit+obit))-1) ^ ((1 << obit)-1))) >> obit << ibit;
                        obit += ibite - ibit;
                        nout += obit >> 3;
                        obit &= 7;
                        ibit = ibite;
                }
        }
        return nout;
}

Я знаю, что это может показаться очевидным, так как я уверен, что на самом деле есть решение, но почему бы не использовать меньший тип, например uint8_t (максимум 255)? или же uint16_t (максимум 65535)?. Я уверен, что вы могли бы немного манипулировать на int64_t используя определенные значения и / или операции и тому подобное, но, кроме академических упражнений, почему?

И на ноте академических упражнений, Bit Twiddling Hacks - хорошее чтение.

Если у вас фиксированные размеры, например, вы знаете, что ваш номер 38 бит, а не 64, вы можете строить структуры, используя битовые спецификации. Забавно, у вас также есть меньшие элементы, чтобы поместиться в оставшееся пространство.

struct example {
    /* 64bit number cut into 3 different sized sections */
    uint64_t big_num:38;
    uint64_t small_num:16;
    uint64_t itty_num:10;

    /* 8 bit number cut in two */
    uint8_t  nibble_A:4;
    uint8_t  nibble_B:4;
};

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

Я не думаю, что вы можете избежать перебора элементов. AFAIK Кодирование Хаффмана требует частот "символов", которые, если вы не знаете статистику "процесса", генерирующего целые числа, вам придется вычислять (путем итерации по каждому элементу).

Другие вопросы по тегам