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

Він використовує подвійний безкоштовний список для досягнення вставки та видалення постійного часу за допомогою одного вільного списку для вільних блоків, готових до вставки (блоки, які не є повними), і підроздільного списку всередині блоку для індексів у цьому блоці готовий бути поверненим після вставки.
Я висвітлю плюси і мінуси цієї структури. Почнемо з деяких мінусів, оскільки їх є декілька:
Мінуси
- Щоб вставити в цю структуру на пару сотень мільйонів елементів, потрібно
std::vector(в чотири рази більше), ніж (чисто суміжна структура). І я досить пристойний у мікрооптимізаціях, але для цього є просто концептуально більше роботи, оскільки звичайний випадок повинен спочатку перевірити вільний блок у верхній частині вільного списку блоків, а потім отримати доступ до блоку та вивести безкоштовний індекс із блоку вільний список, напишіть елемент у вільній позиції, а потім перевірте, чи блок заповнений, і виведіть блок зі списку вільних блоків, якщо так. Це все ще операція постійного часу, але з набагато більшою константою, ніж відштовхування до std::vector.
- Це забирає приблизно вдвічі довший доступ до елементів за допомогою шаблону з випадковим доступом з урахуванням додаткової арифметики для індексації та додаткового шару непрямості.
- Послідовний доступ не відображає ефективно дизайн ітератора, оскільки ітератор повинен виконувати додаткові розгалуження кожного разу, коли він збільшується.
- У нього трохи пам'яті, зазвичай близько 1 біта на елемент. 1 біт на елемент може здатися не дуже схожим, але якщо ви використовуєте це для зберігання мільйона 16-бітних цілих чисел, то це на 6,25% більше використання пам'яті, ніж ідеально компактний масив. Однак на практиці це, як правило, використовує менше пам'яті, ніж
std::vectorякщо ви не ущільнюєте, vectorщоб усунути зайву ємність, яку він резервує. Також я зазвичай не використовую його для зберігання таких підліткових елементів.
Плюси
- Послідовний доступ за допомогою
for_eachфункції, яка приймає діапазони обробки зворотного виклику елементів у блоці, майже конкурує зі швидкістю послідовного доступу std::vector(лише як 10% різниця), тому він не набагато менш ефективний у найбільш критичних для мене випадках використання ( більша частина часу, проведеного в ECS-двигуні, знаходиться в послідовному доступі).
- Це дозволяє видаляти з середини постійний час з блоками розміщення структури, коли вони стають абсолютно порожніми. Як результат, це взагалі цілком пристойно, щоб переконатися, що структура даних ніколи не використовує значно більше пам'яті, ніж потрібно.
- Він не визнає недійсними індекси для елементів, які не вилучаються безпосередньо з контейнера, оскільки він просто залишає отвори, використовуючи підхід у вільному списку, щоб відновити ці отвори після наступного вставки.
- Вам не потрібно так сильно хвилюватися, що не вистачить пам’яті, навіть якщо ця структура містить епічну кількість елементів, оскільки вона вимагає лише невеликих суміжних блоків, які не ставлять перед ОС проблему, щоб знайти величезну кількість суміжних невикористаних сторінок.
- Він добре піддається одночасності та безпеці потоку без блокування всієї структури, оскільки операції, як правило, локалізовані на окремих блоках.
Тепер одним з найбільших плюсів для мене було те, що це стало банальним зробити незмінну версію цієї структури даних, як це:

З тих пір це відкривало всі види дверей для написання додаткових функцій, позбавлених побічних ефектів, що полегшувало досягнення винятків - безпеку, безпеку ниток тощо. Незмінність - це щось, що я виявив, що я міг легко досягти ця структура даних заднім числом і випадково, але, мабуть, одна з найприємніших переваг, яку вона отримала, оскільки значно полегшила підтримку бази даних коду.
У безперервних масивах немає кеш-локації, що призводить до поганої продуктивності. Однак при розмірі блоку 4M, схоже, буде достатньо місця для хорошого кешування.
Місцевість довідників - це не те, чим слід займатися у блоках такого розміру, не кажучи вже про 4-кілобайтних блоках. Рядок кеша зазвичай становить всього 64 байти. Якщо ви хочете зменшити пропуски кешу, то просто зосередитесь на правильному вирівнюванні цих блоків та надайте перевагу більше послідовних моделей доступу, коли це можливо.
Дуже швидкий спосіб перетворити схему пам'яті з випадковим доступом у послідовну - використовувати біт. Скажімо, у вас є індекс індексу, і вони у випадковому порядку. Ви можете просто орати їх і позначати біти в бітах. Потім ви можете перебирати свій біт і перевіряти, які байти не нульові, перевіряючи, скажімо, 64-бітові одночасно. Як тільки ви зіткнетеся з набором 64 біт, з яких встановлено щонайменше один біт, ви можете скористатися інструкціями FFS для швидкого визначення того, які біти встановлені. Біти повідомляють вам, до яких індексів слід отримати доступ, за винятком випадків, коли ви отримуєте індекси, відсортовані в послідовному порядку.
Це має певні накладні витрати, але може бути корисним обміном у деяких випадках, особливо якщо ви збираєтеся перебирати ці показники багато разів.
Доступ до елемента не такий простий, є додатковий рівень непрямості. Це оптимізуватиметься? Чи це призведе до проблем із кешем?
Ні, його неможливо оптимізувати. Принаймні, випадковий доступ, принаймні, завжди буде коштувати дорожче з цією структурою. Це часто не збільшить ваш кеш-промах настільки сильно, хоча оскільки ви прагнете отримувати високу тимчасову локальність з масивом покажчиків на блоки, особливо якщо ваші загальні шляхи виконання випадків використовують послідовні схеми доступу.
Оскільки після досягнення межі 4M спостерігається лінійний ріст, у вас може бути набагато більше виділень, ніж зазвичай (скажімо, максимум 250 виділень на 1 ГБ пам'яті). Після 4М жодна зайва пам'ять не копіюється, проте я не впевнений, що додаткові асигнування дорожчі, ніж копіювання великих шматів пам'яті.
На практиці копіювання часто швидше, тому що це рідкісний випадок, лише трапляється щось на кшталт log(N)/log(2)загального часу, одночасно спрощуючи звичайний випадок із забрудненням, коли ви можете просто записати елемент у масив багато разів, перш ніж він стане повним і його потрібно перерозподілити знову. Тому, як правило, ви не отримаєте швидших вставок із таким типом структури, тому що звичайна робота у випадку є дорожчою, навіть якщо їй не доведеться мати справу з тим дорогим рідкісним випадком перерозподілу величезних масивів.
Основна привабливість цієї структури для мене, незважаючи на всі мінуси, - це зменшення використання пам’яті, не турбуючись про OOM, можливість зберігати індекси та покажчики, які не стають недійсними, паралельністю та незмінністю. Приємно мати структуру даних, куди ви можете вставляти та видаляти речі за постійний час, поки вона сама очищає вас і не приводить до недійсності покажчиків та індексів у структурі.