Якщо 32-бітне ціле число переповнюється, чи можна використовувати 40-бітну структуру замість 64-бітної?


76

Якщо, скажімо, 32-бітове ціле число переповнюється, замість того, щоб оновити intдо long, чи можемо ми використовувати якийсь 40-бітний тип, якщо нам потрібен діапазон лише в межах 2 40 , так що ми зберігаємо 24 (64-40) біти для кожного ціле число?

Якщо так, то як?

Я маю справу з мільярдами, і космос є більшим обмеженням.


5
Також пам'ять дуже дешева в порівнянні з процесорними циклами, щоб зберегти ці дорогоцінні байти
Ед Хіл

9
@ user1810087, Aniket ... звідки ти знаєш, що це непотрібно? Або що він споживає більше байтів, ніж його збереження? Чи знаєте ви вимоги та обмеження? Можливо, він обробляє ТБ даних, де ці "кілька байтів" складаються?
Greenflow

24
@Aniket: Я бачу для цього деякі варіанти використання, особливо при роботі з великими наборами даних. Зараз я працюю з об'ємним моделюванням у кубі 1024 ^ 3. Ми впровадили власний 36-бітний тип даних, оскільки це робить різницю в тому, чи можна використовувати наш додаток із 8 Гб оперативної пам'яті чи ні. Приклад: 1024 ^ 3 куб із 64 біт = 8192 МБ, 36 біт = 4608 біт. У цьому випадку додатковий код більше не має значення.
BDL

5
Існує кілька процесорів, які реалізують в апаратних 40-бітних цілих числах (Наприклад: деякі процесори Texas Instruments). Якщо ви працюєте на одному з цих процесорів, я б сказав так, вперед! Але якщо ви використовуєте апаратне забезпечення, таке як x86, яке має лише 32 або 64-бітні цілі числа, витрати можуть перевищити переваги використання 40-бітових цілих чисел.
Тревор Бойд Сміт

24
@ All: А як щодо того, щоб дозволити користувачеві 1660982 вирішити, дійсно він цього хоче чи ні? Тут ніхто не знає обсягу даних або того, чи важлива швидкість.
deviantfan

Відповіді:


82

Так, але...

Це, звичайно, можливо , але зазвичай це безглуздо (для будь-якої програми, яка не використовує мільярди цих цифр):

#include <stdint.h> // don't want to rely on something like long long
struct bad_idea
{
    uint64_t var : 40;
};

Тут varдійсно буде ширина 40 біт за рахунок набагато менш ефективного коду, що генерується (виявляється, що "багато" - це дуже неправильно - виміряні накладні витрати складають лише 1-2%, див. Таймінги нижче), і зазвичай безрезультатно. Якщо вам не потрібне інше 24-бітове значення (або 8 і 16-бітове значення), яке ви хочете упакувати в ту саму структуру, вирівнювання втратить усе, що ви можете отримати.

У будь-якому випадку, якщо у вас їх не мільярди, фактична різниця у споживанні пам'яті не буде помітною (але додатковий код, необхідний для управління бітовим полем, буде помітний!).

Примітка:
Питання тим часом оновлено, щоб це справді відображало потрібні мільярди чисел, тому це може бути життєздатною справою, передбачаючи, що ви вживаєте заходів, щоб не втратити прибуток через вирівнювання конструкції та відступів, тобто або зберігаючи щось інше в решті 24 біта або зберігаючи свої 40-бітові значення в структурах по 8 або їх кратних).
Заощаджувати три байти в мільярд разів варто, оскільки для цього потрібно буде помітно менше сторінок пам’яті, що призведе до меншої кількості помилок кеш-пам’яті та TLB, і перш за все до несправностей сторінок (помилка однієї сторінки важить десятки мільйонів інструкцій).

Хоча у наведеному фрагменті не використовуються решта 24 біти (він лише демонструє частину "використовувати 40 бітів"), буде потрібно щось подібне до наступного, щоб дійсно зробити підхід корисним у сенсі збереження пам’яті - передбачається, що у вас дійсно є інші "корисні" дані, які потрібно помістити в діри:

struct using_gaps
{
    uint64_t var           : 40;
    uint64_t useful_uint16 : 16;
    uint64_t char_or_bool  : 8;  
};

Розмір та вирівнювання структури будуть дорівнювати 64-бітному цілому числу, тому нічого не витрачається даремно, якщо ви, наприклад, зробите масив з мільярда таких структур (навіть без використання розширень, специфічних для компілятора). Якщо у вас немає використання 8-бітового значення, ви також можете використовувати 48-бітове та 16-бітове значення (що дає більший запас переповнення).
Крім того, ви можете, за рахунок зручності використання, додати 8 40-бітових значень у структуру (найменше спільне кратне 40 і 64 - 320 = 8 * 40). Звичайно, тоді вашого коду, який отримує доступ до елементів масиву структур, стане набагато більше складнішим (хоча, можливо, можна реалізувати такий, operator[]який відновлює функціональність лінійного масиву і приховує складність структури).

Оновлення:
Написав набір швидких тестів, щоб просто побачити, які накладні витрати мають поля полів (і перевантаження оператора посиланнями на польові поля). Опублікований код (через довжину) на gcc.godbolt.org , тестовий результат на моїй машині Win7-64:

Running test for array size = 1048576
what       alloc   seq(w)  seq(r)  rand(w)  rand(r)  free
-----------------------------------------------------------
uint32_t    0      2       1       35       35       1
uint64_t    0      3       3       35       35       1
bad40_t     0      5       3       35       35       1
packed40_t  0      7       4       48       49       1


Running test for array size = 16777216
what        alloc  seq(w)  seq(r)  rand(w)  rand(r)  free
-----------------------------------------------------------
uint32_t    0      38      14      560      555      8
uint64_t    0      81      22      565      554      17
bad40_t     0      85      25      565      561      16
packed40_t  0      151     75      765      774      16


Running test for array size = 134217728
what        alloc  seq(w)  seq(r)  rand(w)  rand(r)  free
-----------------------------------------------------------
uint32_t    0      312     100     4480     4441     65
uint64_t    0      648     172     4482     4490     130
bad40_t     0      682     193     4573     4492     130
packed40_t  0      1164    552     6181     6176     130

Що можна помітити, так це те, що зайві накладні витрати на польові поля нехтують, але перевантаження оператора посиланням на розрядність як зручність є досить різким (приблизно в 3 рази) при лінійному доступі до даних у кеш-формі. З іншого боку, для довільного доступу це ледве навіть важливо.

Ці терміни дозволяють припустити, що просто використання 64-розрядних цілих чисел було б краще, оскільки вони все-таки швидші в цілому, ніж бітові поля (незважаючи на збільшення кількості пам'яті), але, звичайно, вони не враховують вартість несправностей сторінок із набагато більшими наборами даних. Це може виглядати зовсім по-іншому, коли у вас закінчиться фізична оперативна пам’ять (я цього не тестував).


1
Я думав про те саме, але члени розрядного поля з більш ніж 32 бітами є розширенням gcc і не є частиною стандарту C (спробуйте скласти свій код -Wpedantic).
bitmask

2
Цікаво ... clang тут просто чудово (навіть з -Wpedantic). Як і мій GCC. Чи було це обмеження на 32 біти, можливо, послаблене з C ++ 11?
Деймон

2
Хоча ця відповідь не є помилковою, вона насправді не відповідає на питання.
user694733

9
Крім того, структури, що містять бітові поля, доповнені до вирівнювання структури, яка базується на одиниці розподілу бітових полів. Отже, якщо це спрацює, структура все одно буде доповнена 8 байтами, і ви не заощадите місця.
Chris Dodd

3
У більшості компіляторів можна примусити пакувати байти (це прагма, яка варіюється від компілятора до компілятора), що робить масив структури відповідним чином зменшеним.
Джошуа

54

Ви можете досить ефективно упакувати цілі числа 4 * 40 біт у 160-розрядну структуру, як це:

struct Val4 {
    char hi[4];
    unsigned int low[4];
}

long getLong( const Val4 &pack, int ix ) {
  int hi= pack.hi[ix];   // preserve sign into 32 bit
  return long( (((unsigned long)hi) << 32) + (unsigned long)pack.low[i]);
}

void setLong( Val4 &pack, int ix, long val ) {
  pack.low[ix]= (unsigned)val;
  pack.hi[ix]= (char)(val>>32);
}

Їх знову можна використовувати так:

Val4[SIZE] vals;

long getLong( int ix ) {
  return getLong( vals[ix>>2], ix&0x3 )
}

void setLong( int ix, long val ) {
  setLong( vals[ix>>2], ix&0x3, val )
}

13
Враховується фрагмент коду, який фактично економить пам’ять після заповнення! +1
Бен Войгт,

1
Плюс: це насправді економить простір. Недолік: цей код, мабуть, ДУЖЕ повільний через індексацію.
SamB

2
Можливо, варто використовувати signed char hi[4];явно; plain charможе бути підписаним або без підпису.
Джонатан Леффлер

4
Можливо, краще використовувати uint_least32_tі int_least8_tтут, а не unsigned intта char. unsigned intмає бути принаймні 16 біт. charзавжди буде щонайменше 8 біт, тому там не так багато проблем. Крім того, я б використовував множення замість зсуву бітів для hiчастини значення; це добре визначено, і компілятор може замінити зміщення бітів, якщо це доречно. Крім цього, гарна ідея!
Піт Беккер,

11
@SamB: Не одразу ясно, що це буде "ДУЖЕ" повільно. Річ у тім, що (припускаючи, що компілятор налаштований на агресивну оптимізацію - включаючи вбудовування - як це повинно бути для будь-чого, що пов’язано з «мільярдами» операцій!), Все індексування зводиться до внутрішніх процесорних операцій над реєстрами, що можна зробити за дуже мало циклів (тобто швидкий): зазвичай набагато швидше, ніж отримання кеш-рядка з пам'яті. Оскільки зараз ми отримуємо на 35% менше пам'яті, ніж раніше (завдяки економії місця), ми можемо отримати чистий виграш. (Очевидно, це залежить від багато чого - рекомендується вимірювання :))
psmears

25

Можливо, ви захочете розглянути кодування зі змінною довжиною (VLE)

Імовірно, у вас є десь багато цих номерів (в оперативній пам'яті, на диску, надсилання їх через мережу тощо), а потім беруть їх по одному та виконують певну обробку.

Одним із підходів було б кодування їх за допомогою VLE. З документації протобуфу Google (ліцензія CreativeCommons)

Варінти - це метод серіалізації цілих чисел з використанням одного або декількох байтів. Менші числа займають меншу кількість байтів.

Кожен байт у varint, за винятком останнього байта, має найбільш значущий біт (msb) - це вказує на те, що мають бути ще байти. Нижні 7 бітів кожного байта використовуються для зберігання представлення доповнення числа двох у групах із 7 бітів, спочатку найменш значущою групою.

Так, наприклад, ось число 1 - це один байт, тому msb не встановлено:

0000 0001

А ось 300 - це трохи складніше:

1010 1100 0000 0010

Як ви зрозуміли, що це 300? Спочатку ви скидаєте msb з кожного байта, оскільки це просто для того, щоб повідомити нам, чи досягли ми кінця числа (як бачите, воно встановлюється в першому байті, оскільки у varint більше одного байта)

Плюси

  • Якщо у вас багато малих чисел, ви, ймовірно, в середньому використовуєте менше 40 байт на ціле число. Можливо, значно менше.
  • Ви зможете зберігати більші цифри (з понад 40 бітами) в майбутньому, не платячи штрафу за маленькі

Мінуси

  • Ви платите додатковий біт за кожні 7 значущих бітів ваших чисел. Це означає, що число з 40 значущими бітами потребуватиме 6 байт. Якщо більшість ваших чисел мають 40 значущих розрядів, вам краще скористатися методом бітового поля.
  • Ви втратите можливість легко перейти до числа, враховуючи його індекс (вам потрібно принаймні частково проаналізувати всі попередні елементи масиву, щоб отримати доступ до поточного.
  • Вам знадобиться якась форма декодування, перш ніж робити щось корисне з числами (хоча це справедливо і для інших підходів, наприклад, для бітових полів)

ви можете змінити найменшу одиницю на 16 або 32 біта, щоб ви могли заощадити багато пам'яті, якщо більшість значень перевищує 1 байт, але вміщуються в межах 15 або 31 біта
phuclv

3
Якщо числа, які OP намагається зберегти, розподілені рівномірно, тоді великих чисел набагато більше, ніж малих, і кодування змінної довжини буде контрпродуктивним.
Рассел Борогове

21

(Редагувати: Перш за все - те, що ви хочете, можливо, і це має сенс у деяких випадках; мені доводилося робити подібні речі, коли я намагався зробити щось для виклику Netflix і мав лише 1 Гб пам'яті; друге - це, мабуть, найкраще використовувати масив символів для 40-бітового сховища, щоб уникнути будь-яких проблем із вирівнюванням та необхідності возитися з прагмами структурного пакування; по-третє - ця конструкція передбачає, що у вас все в порядку з 64-бітною арифметикою для проміжних результатів, це лише для великих сховище масивів, яке ви використовували б Int40; четверте: я не отримую всіх припущень, що це погана ідея, просто прочитайте, що люди проходять, щоб упакувати сітчасті структури даних, і це виглядає як дитяча гра для порівняння).

Те, що вам потрібно, - це структура, яка використовується лише для зберігання даних як 40-розрядних ints, але неявно перетворює в int64_t для арифметики. Єдина хитрість полягає у правильному розширенні знака з 40 до 64 біт. Якщо у вас все добре з непідписаними ints, код може бути ще простішим. Це повинно мати змогу розпочати роботу.

#include <cstdint>
#include <iostream>

// Only intended for storage, automatically promotes to 64-bit for evaluation
struct Int40
{
     Int40(int64_t x) { set(static_cast<uint64_t>(x)); } // implicit constructor
     operator int64_t() const { return get(); } // implicit conversion to 64-bit
private:
     void set(uint64_t x)
     {
          setb<0>(x); setb<1>(x); setb<2>(x); setb<3>(x); setb<4>(x);
     };
     int64_t get() const
     {
          return static_cast<int64_t>(getb<0>() | getb<1>() | getb<2>() | getb<3>() | getb<4>() | signx());
     };
     uint64_t signx() const
     {
          return (data[4] >> 7) * (uint64_t(((1 << 25) - 1)) << 39);
     };
     template <int idx> uint64_t getb() const
     {
          return static_cast<uint64_t>(data[idx]) << (8 * idx);
     }
     template <int idx> void setb(uint64_t x)
     {
          data[idx] = (x >> (8 * idx)) & 0xFF;
     }

     unsigned char data[5];
};

int main()
{
     Int40 a = -1;
     Int40 b = -2;
     Int40 c = 1 << 16;
     std::cout << "sizeof(Int40) = " << sizeof(Int40) << std::endl;
     std::cout << a << "+" << b << "=" << (a+b) << std::endl;
     std::cout << c << "*" << c << "=" << (c*c) << std::endl;
}

Ось посилання, щоб спробувати його в прямому ефірі: http://rextester.com/QWKQU25252


Погоджено з @Andreas, це просто з передбачуваним кодегеном, на відміну від відповідей, які використовують бітові поля або покладаються на спеціальну компіляторну упаковку. Ось реалізація constexpr-ified C ++ 17.
ildjarn

16

Ви можете використовувати структуру бітового поля, але це не збереже вам жодної пам'яті:

struct my_struct
{
    unsigned long long a : 40;
    unsigned long long b : 24;
};

Ви можете стиснути будь-яку кратну 8 таких 40-бітових змінних в одну структуру:

struct bits_16_16_8
{
    unsigned short x : 16;
    unsigned short y : 16;
    unsigned short z :  8;
};

struct bits_8_16_16
{
    unsigned short x :  8;
    unsigned short y : 16;
    unsigned short z : 16;
};

struct my_struct
{
    struct bits_16_16_8 a1;
    struct bits_8_16_16 a2;
    struct bits_16_16_8 a3;
    struct bits_8_16_16 a4;
    struct bits_16_16_8 a5;
    struct bits_8_16_16 a6;
    struct bits_16_16_8 a7;
    struct bits_8_16_16 a8;
};

Це заощадить трохи пам'яті (у порівнянні з використанням 8 "стандартних" 64-розрядних змінних), але вам доведеться розділити кожну операцію (і зокрема арифметичну) з кожною з цих змінних на кілька операцій.

Тож оптимізація пам’яті буде «торгуватися» для виконання продуктивності.


@barakmanos: Ви впевнені, що ваша нова версія стає кращою?
Ben Voigt

@BenVoigt: На VC2013 так. У чому я не впевнений на 100%, чи це робиться відповідно до мовного стандарту, чи це залежить від компілятора. Якщо це останнє, тоді a #pragma packповинен виконувати "решту роботи". До речі, тут є й інші питання, наприклад, CHAR_BITякі теоретично можуть бути більше 8, або sizeof(short)які теоретично можуть бути 1 (наприклад, якщо CHAR_BITдорівнює 16). Я вважав за краще, щоб відповідь була простою для читання, а не вказувала на всі ці кутові випадки.
barak manos

1
@MarcGlisse і під 64 ви маєте на увазі 8, тому що sizeofвважає байти.
user253751

1
@Inverse: Дякую, але ваші зміни першої частини зробили заяву про відкриття другої частиною безглуздого . До того ж (і навіть гірше) це було неправильно - sizeof(my_struct)це не 5 байт на кожному компіляторі (або, можливо, на будь-якому компіляторі). І в будь-якому випадку, ви не можете створити екземпляр масиву тих структур, які відображатимуть 5 байт на запис. Будь ласка, перевірте свої зміни, перш ніж вносити їх (зокрема, у відповіді інших користувачів).
barak manos

@immibis Ні, я справді мав на увазі 64, але цей коментар був опублікований перед редагуванням (якщо ви хочете побачити, про що мова), перегляньте історію.
Marc Glisse

9

Як зазначають коментарі, це цілком завдання.

Можливо, непотрібні клопоти, якщо ви не хочете заощадити багато оперативної пам'яті - тоді це має набагато більше сенсу. (Збереження оперативної пам'яті буде загальною сумою бітів, збережених у мільйонах longзначень, що зберігаються в оперативній пам'яті)

Я б розглянув можливість використання масиву з 5 байт / char (5 * 8 біт = 40 біт). Тоді вам потрібно буде перекласти біти з вашого (переповненого int - отже, а long) значення в масив байтів, щоб зберегти їх.

Щоб використовувати значення, перекладіть біти назад у a, longі ви можете використовувати значення.

Тоді пам'ять вашої оперативної пам'яті та файлів буде 40 біт (5 байтів), АЛЕ ви повинні врахувати вирівнювання даних, якщо плануєте використовувати a structдля утримання 5 байт. Повідомте мене, якщо вам потрібна детальна робота щодо цього зміщення бітів та наслідків вирівнювання даних.

Подібним чином, ви можете використовувати 64-біт longі приховувати інші значення (можливо, 3 символи) у залишкових 24 бітах, які ви не хочете використовувати. Знову ж таки - використання зсуву бітів для додавання та видалення 24 бітових значень.


6

Ще однією варіацією, яка може бути корисною, є використання структури:

typedef struct TRIPLE_40 {
  uint32_t low[3];
  uint8_t hi[3];
  uint8_t padding;
};

Така структура займе 16 байт і, якщо вирівняти 16 байт, вона повністю поміститься в одну лінію кешу. Виявляючи, яку з частин структури використовувати, може бути дорожче, ніж було б, якби структура містила чотири елементи замість трьох, доступ до однієї лінії кешу може бути набагато дешевшим, ніж доступ до двох. Якщо продуктивність важлива, слід використовувати деякі контрольні показники, оскільки деякі машини можуть виконувати операцію divmod-3 дешево і мати високу вартість за вибірку кеш-лінії, тоді як інші можуть мати дешевший доступ до пам'яті та дорожчий divmod-3.


Зауважте, що divmod-3, можливо, насправді було б здійснено множенням.
SamB

@SamB: Це справді зазвичай найкраще робити з якимсь множенням, але це може різнитися залежно від реалізації. Щось на зразок Cortex-M0, divmod3 довільного 32-бітового числа був би дещо дорогим, і якщо б виконувались повністю окремі вибірки для 32-бітових частин і 40-бітових частин числа, не було б проблемою.
supercat

6

Я припускаю це

  1. це С, і
  2. вам потрібен один великий масив із 40 бітових чисел і
  3. ви знаходитесь на машині, яка є мало-ендіанською, і
  4. ваша машина достатньо розумна для вирівнювання
  5. Ви визначили розмір як кількість 40-розрядних чисел, яка вам потрібна

unsigned char hugearray[5*size+3];  // +3 avoids overfetch of last element

__int64 get_huge(unsigned index)
{
    __int64 t;
    t = *(__int64 *)(&hugearray[index*5]);
    if (t & 0x0000008000000000LL)
        t |= 0xffffff0000000000LL;
    else
        t &= 0x000000ffffffffffLL;
    return t;
}

void set_huge(unsigned index, __int64 value)
{
    unsigned char *p = &hugearray[index*5];
    *(long *)p = value;
    p[4] = (value >> 32);
}

Можливо, швидше впоратися з отриманням у дві зміни.

__int64 get_huge(unsigned index)
{
    return (((*(__int64 *)(&hugearray[index*5])) << 24) >> 24);
}

4
Зверніть увагу, що код містить невизначену поведінку, оскільки unsigned charне гарантується правильне вирівнювання для __int64. На деяких платформах, таких як x86-64, це, мабуть, не матиме великого впливу на неоптимізовану збірку (очікуйте досягнення продуктивності), але на інших це проблематично - наприклад, ARM. В оптимізованих збірках всі ставки вимикаються, оскільки компілятор може, наприклад, створювати код за допомогою movaps.
Maciej Piechotka

1
Мабуть, найпростіше рішення з усіх!
anatolyg

Звичайно, це виглядає якось потворно на мові C з усіма видами типів, але отриманий машинний код буде простим і швидким. Ваша shift-версія get, швидше за все, швидша, оскільки не розгалужується. Його можна додатково оптимізувати, читаючи з 3 байт перед числом, тим самим зберігаючи зміщення вліво.
aaaaaaaaaaaa

1
Ви можете залишити до компілятора зробити ефективне розширення знаків таким чином . Однак це слід ретельно перевірити, оскільки не узгоджений доступ може коштувати дуже дорого. Зберігання 5-го байта окремо, як і в деяких інших рішеннях, може бути кращим
phuclv

1
Ви можете використовувати memcpyдля портативного висловлення незрівняних вантажів / магазинів , без будь-яких суворих згладжувальних порушень, таких як те, що використовується для вказівника. Сучасні компілятори, орієнтовані на x86 (або на інші платформи з ефективними незрівнянними навантаженнями), просто використовуватимуть вирівняне навантаження або сховище. Наприклад, ось ( godbolt.org/g/3BFhWf ) зламана версія 40-розрядного цілочисельного класу C ++ Деймона, яка використовує a char value[5]та компілюється до того ж asm, що і цей gcc для x86-64. (Якщо ви використовуєте версію, яка перечитує, замість того, щоб робити окремі завантаження, але це теж досить добре)
Пітер Кордес,

5

Для випадку збереження декількох мільярдів 40-бітових цілих чисел із підписом і, припускаючи 8-бітові байти, ви можете упакувати 8 40-бітних підписаних цілих чисел у структуру (у коді нижче, використовуючи для цього масив байтів), і, оскільки ця структура зазвичай вирівнюється, ви можете створити логічний масив таких упакованих груп і забезпечити звичайне послідовне індексування цього:

#include <limits.h>     // CHAR_BIT
#include <stdint.h>     // int64_t
#include <stdlib.h>     // div, div_t, ptrdiff_t
#include <vector>       // std::vector

#define STATIC_ASSERT( e ) static_assert( e, #e )

namespace cppx {
    using Byte = unsigned char;
    using Index = ptrdiff_t;
    using Size = Index;

    // For non-negative values:
    auto roundup_div( const int64_t a, const int64_t b )
        -> int64_t
    { return (a + b - 1)/b; }

}  // namespace cppx

namespace int40 {
    using cppx::Byte;
    using cppx::Index;
    using cppx::Size;
    using cppx::roundup_div;
    using std::vector;

    STATIC_ASSERT( CHAR_BIT == 8 );
    STATIC_ASSERT( sizeof( int64_t ) == 8 );

    const int bits_per_value    = 40;
    const int bytes_per_value   = bits_per_value/8;

    struct Packed_values
    {
        enum{ n = sizeof( int64_t ) };
        Byte bytes[n*bytes_per_value];

        auto value( const int i ) const
            -> int64_t
        {
            int64_t result = 0;
            for( int j = bytes_per_value - 1; j >= 0; --j )
            {
                result = (result << 8) | bytes[i*bytes_per_value + j];
            }
            const int64_t first_negative = int64_t( 1 ) << (bits_per_value - 1);
            if( result >= first_negative )
            {
                result = (int64_t( -1 ) << bits_per_value) | result;
            }
            return result;
        }

        void set_value( const int i, int64_t value )
        {
            for( int j = 0; j < bytes_per_value; ++j )
            {
                bytes[i*bytes_per_value + j] = value & 0xFF;
                value >>= 8;
            }
        }
    };

    STATIC_ASSERT( sizeof( Packed_values ) == bytes_per_value*Packed_values::n );

    class Packed_vector
    {
    private:
        Size                    size_;
        vector<Packed_values>   data_;

    public:
        auto size() const -> Size { return size_; }

        auto value( const Index i ) const
            -> int64_t
        {
            const auto where = div( i, Packed_values::n );
            return data_[where.quot].value( where.rem );
        }

        void set_value( const Index i, const int64_t value ) 
        {
            const auto where = div( i, Packed_values::n );
            data_[where.quot].set_value( where.rem, value );
        }

        Packed_vector( const Size size )
            : size_( size )
            , data_( roundup_div( size, Packed_values::n ) )
        {}
    };

}    // namespace int40

#include <iostream>
auto main() -> int
{
    using namespace std;

    cout << "Size of struct is " << sizeof( int40::Packed_values ) << endl;
    int40::Packed_vector values( 25 );
    for( int i = 0; i < values.size(); ++i )
    {
        values.set_value( i, i - 10 );
    }
    for( int i = 0; i < values.size(); ++i )
    {
        cout << values.value( i ) << " ";
    }
    cout << endl;
}

Я думаю, ви передбачаєте доповнення 2 для розширення знаку. Він вважає, що це розривається зі знаком / величиною, але може працювати з доповненням 1. У будь-якому випадку, для доповнення 2, напевно, було б простіше і ефективніше попросити компілятор підписати-розширити для вас останній байт до 64 бітів, а потім АБО в нижній половині. (Тоді компілятори x86 могли б використовувати movsxбайт-навантаження, зсув, а потім АБО у низьких 32 бітах. Більшість інших архітектур також мають вузькі навантаження, що розширюють знак). роби що хочеш.
Пітер Кордес,

@PeterCordes: Дякую, там є не згадуване припущення про форму доповнення двох, так. Не знаю, чому я на це покладався. Спантеличує.
Вітаю і hth. - Альф

Я б не жертвував ефективністю лише для того, щоб зробити його портативним для платформ, на яких ніхто ніколи його не використовуватиме. Але по можливості використовуйте static_assertдля перевірки семантики, на яку ви покладаєтесь.
Пітер Кордес,

5

Якщо вам доводиться мати справу з мільярдами цілих чисел, я спробую включити масиви з 40-бітових чисел замість одинарних 40-розрядних чисел. Таким чином, ви можете протестувати різні реалізації масиву (наприклад, реалізацію, яка стискає дані на льоту, або, можливо, таку, що зберігає менш використовувані дані на диску.), Не змінюючи решту коду.

Ось зразок реалізації ( http://rextester.com/SVITH57679 ):

class Int64Array
{
    char* buffer;
public:
    static const int BYTE_PER_ITEM = 5;

    Int64Array(size_t s)
    {
        buffer=(char*)malloc(s*BYTE_PER_ITEM);
    }
    ~Int64Array()
    {
        free(buffer);
    }

    class Item
    {
        char* dataPtr;
    public:
        Item(char* dataPtr) : dataPtr(dataPtr){}

        inline operator int64_t()
        {
            int64_t value=0;
            memcpy(&value, dataPtr, BYTE_PER_ITEM); // Assumes little endian byte order!
            return value;
        }

        inline Item& operator = (int64_t value)
        {
            memcpy(dataPtr, &value, BYTE_PER_ITEM); // Assumes little endian byte order!
            return *this;
        }
    };   

    inline Item operator[](size_t index) 
    {
        return Item(buffer+index*BYTE_PER_ITEM);
    }
};

Примітка: memcpy-конверсія з 40-бітової на 64-бітову в основному є невизначеною поведінкою, оскільки вона передбачає невелику ендіанність. Однак він повинен працювати на x86-платформах.

Примітка 2: Очевидно, що це код доказової концепції, а не готовий до виробництва код. Щоб використовувати його в реальних проектах, вам слід додати (серед іншого):

  • обробка помилок (malloc може вийти з ладу!)
  • конструктор копіювання (наприклад, скопіювавши дані, додавши підрахунок посилань або зробивши конструктор копіювання приватним)
  • конструктор переміщення
  • const перевантаження
  • STL-сумісні ітератори
  • обмеження перевірок на індекси (у збірці налагодження)
  • перевірка діапазону значень (у збірці налагодження)
  • твердження для неявних припущень (мало-ендіанська)
  • В даний час Itemмає посилальну семантику, а не семантику значення, що є незвичним для operator[]; Можливо, ви могли б обійти це за допомогою деяких розумних прийомів перетворення типу C ++

Все це повинно бути просто для програміста на C ++, але вони могли б зробити зразок коду набагато довшим, не роблячи його зрозумілішим, тому я вирішив їх опустити.


@anatolyg: Я намагався узагальнити ваші пункти в Примітці 2. Ви можете додати до цього списку ;-)
Нікі

3

Так, ви можете це зробити, і це заощадить трохи місця для великої кількості чисел

Вам потрібен клас, який містить std :: vector безцільного цілого типу.

Вам будуть потрібні функції-члени для зберігання та отримання цілого числа. Наприклад, якщо ви хочете зберегти 64 цілі числа по 40 бітів кожне, використовуйте вектор із 40 цілих чисел по 64 біти. Тоді вам потрібен метод, що зберігає ціле число з індексом у [0,64], і метод отримання такого цілого числа.

Ці методи будуть виконувати деякі операції зсуву, а також деякі двійкові | та &.

Я поки що не додаю більше деталей, оскільки ваше запитання не дуже конкретне. Чи знаєте ви, скільки цілих чисел ви хочете зберегти? Чи знаєте ви це під час компіляції? Чи знаєте ви це, коли програма запускається? Як слід організувати цілі числа? Як масив? Подобається карта? Ви повинні знати все це перед тим, як намагатися стиснути цілі числа в менше місця.


40 * 64 = 2560 біт можна зменшити до lcm (40,64) = 320 біт на "блок", тобто. 5 64-бітних
дюймів

3
std::vector<>це точно не той шлях: він має відбиток щонайменше трьох покажчиків, тобто 96 або 192 біта, залежно від архітектури. Це набагато гірше, ніж 64 біти a long long.
cmaster - відновити моніку

3
Залежить. Один std :: vector для 100000000 цілих чисел - це нормально. Якщо ми хочемо розробляти невеликі блоки, як в іншій відповіді, std :: vector буде марною тратою місця.
Hans Klünder

3

Тут є досить багато відповідей, що стосуються реалізації, тому я хотів би поговорити про архітектуру.

Зазвичай ми розширюємо 32-розрядні значення до 64-розрядних, щоб уникнути переповнення, оскільки наші архітектури розроблені для обробки 64-розрядних значень.

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

Як приклад того, наскільки це важливо, специфікація C ++ 11 визначає багатопотокові випадки гонок на основі "розташування пам'яті". Розташування пам'яті визначено в 1.7.3:

Розташування пам’яті - це або об’єкт скалярного типу, або максимальна послідовність сусідніх бітових полів, що мають ненульову ширину.

Іншими словами, якщо ви використовуєте бітові поля C ++, вам доведеться ретельно виконувати всі свої багатопотокові роботи. Два сусідні бітові поля повинні розглядатися як одне і те ж розташування пам'яті, навіть якщо ви хочете, щоб обчислення по них могли бути розподілені по декількох потоках. Це дуже незвично для С ++, тому, ймовірно, це спричинить розлад розробника, якщо вам доведеться про це турбуватися.

Більшість процесорів мають архітектуру пам'яті, яка одночасно отримує 32-розрядні або 64-розрядні блоки пам'яті. Таким чином, використання 40-бітових значень матиме дивовижну кількість додаткових звернень до пам'яті, що суттєво вплине на час роботи. Розглянемо питання вирівнювання:

40-bit word to access:   32-bit accesses   64bit-accesses
word 0: [0,40)           2                 1
word 1: [40,80)          2                 2
word 2: [80,120)         2                 2
word 3: [120,160)        2                 2
word 4: [160,200)        2                 2
word 5: [200,240)        2                 2
word 6: [240,280)        2                 2
word 7: [280,320)        2                 1

У 64-бітовій архітектурі одне з кожних 4 слів буде "нормальною швидкістю". Решта вимагатиме отримання вдвічі більше даних. Якщо ви отримуєте багато помилок кешу, це може знищити продуктивність. Навіть якщо ви отримуєте звернення до кешу, вам доведеться розпакувати дані та перепакувати їх у 64-розрядний регістр, щоб використовувати їх (що навіть може передбачати важко передбачувану гілку).

Цілком можливо, це варте витрат

Бувають ситуації, коли ці покарання є прийнятними. Якщо у вас є великий обсяг даних, які постійно зберігаються в пам'яті, які добре проіндексовані, ви можете виявити, що економія пам'яті вартує покарання за продуктивність. Якщо ви робите велику кількість обчислень для кожного значення, ви можете виявити, що витрати є мінімальними. Якщо так, сміливо впроваджуйте одне з наведених вище рішень. Однак ось кілька рекомендацій.

  • Не використовуйте бітові поля, якщо ви не готові оплатити їх вартість. Наприклад, якщо у вас є масив бітових полів і ви хочете розділити його на обробку між кількома потоками, ви застрягли. Згідно з правилами C ++ 11, всі бітові поля утворюють одне розташування пам'яті, тому одночасно може отримати доступ лише один потік (це тому, що метод упаковки бітових полів визначений реалізацією, тому C ++ 11 не може допомогти вам розповсюдити їх у визначеному нереалізацією порядку)
  • Не використовуйте структуру, що містить 32-розрядне ціле число та символ, щоб зробити 40 байт. Більшість процесорів забезпечать вирівнювання, і ви не збережете жодного байта.
  • Використовуйте однорідні структури даних, такі як масив символів або масив 64-розрядних цілих чисел. Це набагато простіше , щоб отримати вирівнювання правильно. (І ви також зберігаєте контроль над упаковкою, а це означає, що ви можете розділити масив між кількома потоками для обчислення, якщо будете обережні)
  • Створіть окремі рішення для 32-розрядних та 64-розрядних процесорів, якщо вам потрібно підтримувати обидві платформи. Оскільки ви робите щось на дуже низькому рівні і дуже погано підтримується, вам доведеться власноруч адаптувати кожен алгоритм до його архітектури пам'яті.
  • Пам’ятайте, що множення 40-бітових чисел відрізняється від множення 64-бітового розширення 40-бітових чисел, зменшених назад до 40-бітових. Так само, як і при роботі з x87 FPU, ви повинні пам’ятати, що розподіл даних між бітовими розмірами змінює ваш результат.

Якщо ваші номери суміжні (наприклад, struct { char val[5]; };з memcpy), багаторазові завантаження або збереження будуть до одного рядка кешу. Це дешево (якщо раніше ви не були вузьким місцем за інструкцією або пропускною здатністю L1D), і це не призведе до зайвих помилок кешу, але переможе автоматичну векторизацію, тому ви навіть не зможете встигати за пам’яттю для послідовного доступу. (Зазвичай ви очікуєте, що він компілюється до 32-бітового + 8-бітового навантаження для цілей, що підтримують незрівнянне навантаження. Сучасний x86 має низькі покарання за розбиття рядків кешу, хоча, коли завантаження слова розділяється на 4k-сторінці, покарання вище).
Пітер Кордес,

Стратегія упаковки / розпакування, що включає гілку, можлива, але ледве варто згадувати, якщо ви вручну не отримаєте супер низького рівня з uintptr_tконтролями та вирівнюванням / широкими навантаженнями ( як ви могли б розглянути в asm ). Або ви говорили про те, щоб зробити це поверх uint64_t []і використовувати a, ifщоб зрозуміти, чи потрібно вам лише одне завантаження? Це звучить як погана ідея проти простого використання зсувів для розділення / об’єднання uint64_t в / з uint32_tта uint8_t, а також memcpy або використання структури для групування для вирівнювання.
Пітер Кордес,

Відповідно до ISO C ++ 11, ви можете розмежувати "розташування пам'яті" за допомогою бітового поля нульової ширини. Я не впевнений, що стандарт передбачає, що масив struct __attribute__((packed)) { unsigned long long v:40; };дійсно був би єдиним гігантським місцем пам'яті; але навіть якщо межі структури не є межами розташування пам’яті, ви можете скористатися цим, int end:0щоб гарантувати це ( помилки компілятора за модулем ! та stackoverflow.com/questions/47008183/… )
Пітер Кордес,

3

Це вимагає потокового стиснення без втрат у пам'яті. Якщо це стосується програми Big Data, то щільні трюки для упаковки - це тактичні рішення, в кращому випадку для того, що, здається, вимагає досить пристойної підтримки середнього програмного забезпечення або системного рівня. Їм знадобиться ретельне тестування, щоб переконатися, що хтось може відновити всі біти неушкодженими. І наслідки для продуктивності вкрай нетривіальні і дуже залежні від апаратного забезпечення через перешкоди архітектурі кешування процесора (наприклад, лінії кешування проти структури упаковки). Хтось згадав про складні структури з’єднання: вони часто налаштовані для співпраці з певними архітектурами кешування.

З вимог не зрозуміло, чи потрібен ОП довільний доступ. Враховуючи розмір даних, швидше за все, потрібен буде лише локальний довільний доступ на відносно невеликих фрагментах, організованих ієрархічно для пошуку. Навіть апаратне забезпечення робить це при великих обсягах пам'яті (NUMA). Як показують формати фільмів без втрат, має бути можливість отримати довільний доступ шматками ('кадри') без необхідності завантажувати весь набір даних у гарячу пам'ять (із стиснутого сховища в пам'яті).

Я знаю одну швидку систему баз даних (kdb від KX Systems, щоб назвати одну, але я знаю, що існують і інші), яка може обробляти надзвичайно великі набори даних, мабуть, картографуючи великі набори даних із резервного сховища. Він має можливість прозоро стискати та розширювати дані на льоту.


2

Якщо вам дійсно потрібен масив із 40 бітових цілих чисел (чого, очевидно, у вас не може бути), я просто поєднав би один масив із 32 бітами та один із 8 бітових цілих чисел.

Щоб прочитати значення x за індексом i:

uint64_t x = (((uint64_t) array8 [i]) << 32) + array32 [i];

Щоб записати значення x в індекс i:

array8 [i] = x >> 32; array32 [i] = x;

Очевидно, приємно інкапсульований у клас за допомогою вбудованих функцій для максимальної швидкості.

Існує одна ситуація, коли це неоптимально, і це коли ви виконуєте справді випадковий доступ до багатьох елементів, так що кожен доступ до масиву int буде пропущеним кешем - тут ви будете отримувати дві помилки кешу щоразу. Щоб цього уникнути, визначте 32-байтову структуру, що містить масив із шести uint32_t, масив із шести uint8_t та два невикористані байти (41 2/3 біт на число); код доступу до елемента дещо складніший, але обидва компоненти елемента знаходяться в одному рядку кешу.


Хіба це не зробить жахливих речей для кеш-пам'яті?
SamB
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.