Любой способ "вычленить" общие поля для экономии места? - PullRequest
0 голосов
/ 13 мая 2018

У меня есть большой массив (> миллионов) Item с, где каждый Item имеет форму:

struct Item { void *a; size_t b; };

Существует несколько отдельных полей a, то есть много элементов с одинаковым полем a.

Я бы хотел «разложить» эту информацию, чтобы сэкономить около 50% использования памяти.

Однако проблема в том, что эти Item имеют значительный порядок , и это может со временем измениться. Поэтому я не могу просто сделать отдельное Item[] для каждого отдельного a, потому что это потеряет относительное упорядочение элементов относительно друг друга.

С другой стороны, если я сохраню упорядочения всех элементов в поле size_t index;, я потеряю экономию памяти при удалении поля void *a;.

Так есть ли у меня способ на самом деле сохранить память здесь или нет?

(Примечание: я уже могу подумать, например, об использовании unsigned char для a для индексации в небольшой массив, но мне интересно, есть ли лучший способ. Для этого потребуется либо использовать невыровненную память, либо делить каждые Item[] на две части, что не очень хорошо для локальности памяти, поэтому я бы предпочел что-то еще.)

Ответы [ 5 ]

0 голосов
/ 14 мая 2018

Это что-то вроде хака, но я использовал его в прошлом с некоторым успехом. Дополнительные затраты на доступ к объекту были компенсированы значительным сокращением памяти.

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

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

На практике теперь, когда выровненный доступ не требуется большинству основных процессоров, я бы просто использовал упакованную структуру вместо следующего хака. Если вы не платите за невыровненный доступ, то оптимальным является хранение {однобайтового типа + восьмибайтового значения} в виде девяти смежных байтов; единственная цена - умножение на 9 вместо 8 для индексированного доступа, и это тривиально, поскольку 9 - это константа времени компиляции.

Если вам необходимо заплатить за неприсоединившийся доступ, возможно следующее. Векторы «дополненных» значений имеют тип:

// Assume that Payload has already been typedef'd. In my application,
// it would be a union of, eg., uint64_t, int64_t, double, pointer, etc.
// In your application, it would be b.

// Eight-byte payload version:
typedef struct Chunk8 { uint8_t kind[8]; Payload value[8]; }

// Four-byte payload version:
typedef struct Chunk4 { uint8_t kind[4]; Payload value[4]; }

Векторы - это векторы Чанков. Чтобы хак работал, они должны быть распределены по 8- (или 4-) байтовым адресам памяти, но мы уже предположили, что выравнивание требуется для типов полезной нагрузки.

Ключ к взлому - то, как мы представляем указатель на отдельное значение, потому что значение не является непрерывным в памяти. Мы используем указатель на его kind член в качестве прокси:

typedef uint8_t ValuePointer;

А затем используйте следующие функции с низким, но не нулевым уровнем издержек:

#define P_SIZE 8U
#define P_MASK P_SIZE - 1U
// Internal function used to get the low-order bits of a ValuePointer.
static inline size_t vpMask(ValuePointer vp) {
  return (uintptr_t)vp & P_MASK;
}
// Getters / setters. This version returns the address so it can be
// used both as a getter and a setter
static inline uint8_t* kindOf(ValuePointer vp) { return vp; }
static inline Payload* valueOf(ValuePointer vp) {
  return (Payload*)(vp + 1 + (vpMask(vp) + 1) * (P_SIZE - 1));
}

// Increment / Decrement
static inline ValuePointer inc(ValuePointer vp) {
  return vpMask(++vp) ? vp : vp + P_SIZE * P_SIZE;
}

static inline ValuePointer dec(ValuePointer vp) {
  return vpMask(vp--) ? vp - P_SIZE * P_SIZE : vp;
}

// Simple indexed access from a Chunk pointer
static inline ValuePointer eltk(Chunk* ch, size_t k) {
  return &ch[k / P_SIZE].kind[k % P_SIZE];
}

// Increment a value pointer by an arbitrary (non-negative) amount
static inline ValuePointer inck(ValuePointer vp, size_t k) {
  size_t off = vpMask(vp);
  return eltk((Chunk*)(vp - off), k + off);
}

Я пропустил кучу других хаков, но я уверен, что вы можете их выяснить.

Одна замечательная вещь при чередовании фрагментов значения - это то, что он имеет умеренно хорошее местоположение. Для 8-байтовой версии почти половину времени произвольный доступ к виду и значению будет попадать только на одну 64-байтовую строку кэширования; в остальное время выполняются две последовательные линии кеша, в результате чего перемещение вперед (или назад) по вектору столь же благоприятно для кеширования, как и при прохождении по обычному вектору, за исключением того, что он использует меньше кешлайнов, поскольку объекты имеют половину размера , Четырехбайтовая версия даже удобнее для кэша.

0 голосов
/ 14 мая 2018

Подход Array-of-Structures может быть полезен. То есть есть три вектора ...

vector<A> vec_a;
vector<B> vec_b;
SomeType  b_to_a_map;

Вы получаете доступ к своим данным как ...

Item Get(int index)
{
    Item retval;
    retval.a = vec_a[b_to_a_map[index]];
    retval.b = vec_b[index];
    return retval;
}

Теперь все, что вам нужно сделать, это выбрать что-то разумное для SomeType. Например, если vec_a.size () равны 2, вы можете использовать vector или boost :: dynamic_bitset. В более сложных случаях вы можете попробовать битовую упаковку, например, для поддержки 4-х значений A, мы просто меняем нашу функцию с ...

int a_index = b_to_a_map[index*2]*2 + b_to_a_map[index*2+1];
retval.a = vec_a[a_index];

Вы всегда можете превзойти битовую упаковку, используя диапазонную упаковку, используя div / mod для хранения дробной битовой длины на элемент, но сложность быстро растет.

Хорошее руководство можно найти здесь http://number -none.com / product / Packing% 20Integers / index.html

0 голосов
/ 13 мая 2018

Используйте комбинацию облегченных схем сжатия (см. this для примеров и некоторых ссылок) для представления значений a*.Ответ @ Фрэнка использует DICT, за которым следует NS, например.Если у вас длинные прогоны с одним и тем же указателем, вы можете рассмотреть RLE (кодирование длин серий) поверх этого.

0 голосов
/ 13 мая 2018

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

Здесь действительно важно число различных Item s, то есть количество различных (a,b) пар.В конце концов, может случиться так, что для одного a существует миллиард разных b с, а для других - всего несколько, поэтому вы хотите воспользоваться этим.

Если мы предположим, что есть на выбор N различных элементов, то нам нужно n = ceil(log2(N)) бит для представления каждого Item.Итак, что нам действительно нужно, это массив n -битных целых чисел, с n, вычисленным в время выполнения .Затем, как только вы получите n -битное целое число, вы можете выполнить бинарный поиск за log(n) время, чтобы выяснить, какому a оно соответствует, основываясь на ваших знаниях о количестве b с для каждого a.(Это может немного сказаться на производительности, но это зависит от количества отдельных a с.)

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

Предостережение заключается в том, что деление на переменную, вероятно, серьезно повредитпроизводительность произвольного доступа (хотя она все равно будет O (1)).Чтобы смягчить эту проблему, можно написать несколько различных процедур для общих значений n (здесь помогают шаблоны C ++!), А затем перейти к ним с различными if (n == 33) { handle_case<33>(i); } или switch (n) { case 33: handle_case<33>(i); } и т. Д., Чтобы компилятор мог видетьделитель как константа и генерирует сдвиги / добавления / умножения по мере необходимости, а не деления.

Это теоретически оптимально для информации, если вам требуется постоянное число бит на элемент, а это то, что вам нужнодля случайного доступа.Однако вы могли бы добиться большего, если бы ослабили это ограничение: вы можете упаковать несколько целых чисел в k * n биты, а затем извлечь их с большим количеством математических операций.Это, вероятно, тоже снизит производительность.

(Или, если коротко: C и C ++ действительно нужна высокопроизводительная uint_vector структура данных ...)

0 голосов
/ 13 мая 2018

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

Этомышление на правильном пути, но это не так просто, так как вы столкнетесь с некоторыми неприятными проблемами выравнивания / заполнения, которые сведут на нет ваш выигрыш в памяти.

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

#define A_INDEX_BITS 3
struct Item { 
  size_t a_index : A_INDEX_BITS; 
  size_t b       : (sizeof(size_t) * CHAR_BIT) - A_INDEX_BITS; 
};

Обратите внимание, что это ограничит количество битов, доступных для b, но на современных платформах, где sizeof(size_t)8, удаление 3-4 битов из него редко является проблемой.

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