Динамічне розподілення пам’яті та управління пам’яттю


17

У середній грі на сцені є сотні, а може й тисячі предметів. Чи повністю правильно розподіляти пам'ять на всі об'єкти, включаючи постріли з гармати (кулі), динамічно за допомогою default new () ?

Чи варто створити будь-який пул пам’яті для динамічного розподілу , чи не потрібно це турбуватися? Що робити, якщо цільовою платформою є мобільні пристрої?

Чи потрібен менеджер пам'яті в мобільній грі, будь ласка? Дякую.

Використовувана мова: C ++; В даний час розробляється під Windows, але планується перенести пізніше.


Яка мова?
Kylotan

@Kylotan: використовувана мова: C ++, що зараз розробляється під Windows, але планується перенести пізніше.
Bunkai.Satori

Відповіді:


23

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

Це дійсно залежить від того, що ви маєте на увазі під «правильним». Якщо ви сприймаєте цей термін буквально (і ігноруєте будь-яку концепцію коректності конструкції, що мається на увазі), то так, це цілком прийнятно. Ваша програма буде компілювати та працювати добре.

Він може працювати недостатньо оптимально, але він все ще може виконуватись досить добре, щоб бути розвантажувальною і веселою грою.

Чи варто створити будь-який пул пам’яті для динамічного розподілу, чи не потрібно це турбуватися? Що робити, якщо цільовою платформою є мобільні пристрої?

Профілюй і дивись. Наприклад, у C ++, динамічне розподілення на купі, як правило, є "повільною" операцією (оскільки передбачає прогулянку по купі, шукаючи блок відповідного розміру). У C # це, як правило, надзвичайно швидка операція, оскільки вона передбачає трохи більше, ніж збільшення. Різні мовні реалізації мають різні характеристики продуктивності щодо розподілу пам'яті, фрагментації після випуску тощо.

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

Чи потрібен менеджер пам'яті в мобільній грі, будь ласка? Дякую.

Знову профілюйте і дивіться. Зараз ваша гра працює нормально? Тоді вам може не потрібно хвилюватися.

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

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

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

Наприклад, якщо ви вважаєте, що менеджер пам'яті є предметом, який просто перехоплює дзвінки до нового / delete / free / malloc / що завгодно і надає діагностику щодо кількості пам'яті, яку ви виділяєте, що ви протікаєте тощо, то це може бути корисним інструмент для гри, поки він розробляється, щоб допомогти вам налагодити витоки та налаштувати оптимальні розміри пулу пам’яті тощо.


Домовились. Код таким чином, що дозволяє змінити речі пізніше. Якщо ви сумніваєтесь, орієнтир або профіль.
axel22

@ Джош: +1 за відмінну відповідь. Напевно, мені потрібно мати комбінацію динамічного розподілу, статичного розподілу та пулів пам’яті. Однак, ефективність гри допоможе мені в правильному поєднанні цих трьох. Це ясний кандидат на прийняте відповідь на моє запитання. Однак я хотів би залишити питання відкритим на деякий час, щоб побачити, що сприятимуть інші.
Bunkai.Satori

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

@ Чудовий: дякую за ваш коментар. Таким чином, мета - зробити гру робочою і стрибкою. Не потрібно надто турбуватися про продуктивність в середині розробки. Це все можна і буде виправлено після завершення гри.
Bunkai.Satori

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

7

Мені не дуже додати відмінну відповідь Джоша, але я прокоментую це:

Чи варто створити будь-який пул пам’яті для динамічного розподілу, чи не потрібно це турбуватися?

Між пулами пам’яті та викликом newкожного розподілу є середнє місце . Наприклад, ви можете виділити задану кількість об'єктів у масиві, а потім встановити на них прапор, щоб згодом їх "знищити". Коли вам потрібно виділити більше, ви можете перезаписати їх зі знищеним прапором. Такі речі є лише дещо складнішими у використанні, ніж нові / видалення (як у вас є дві нові функції для цієї мети), але вони прості в написанні та можуть принести вам великі вигоди.


+1 за приємне доповнення. Так, ви правильно, це хороший спосіб керування більш простими ігровими елементами, такими як: кулі, частинки, ефекти. Спеціально для тих, що не потрібно було б динамічно розподіляти пам'ять.
Bunkai.Satori

3

Чи повністю правильно розподіляти пам'ять на всі об'єкти, включаючи постріли з гармати (кулі), динамічно за допомогою default new ()?

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

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


+1 за гарну відповідь. Отже, для узагальнення правильний підхід був би: на початку розробки планувати, які об’єкти можна виділити статично. Під час розробки динамічно виділяти лише ті об’єкти, які абсолютно мають бути розподілені динамічно. Зрештою, для профілювання та коригування можливих проблем з розподілом пам'яті.
Bunkai.Satori

0

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

Ось простий приклад того, як можна уникнути виділення та звільнення Foosбагаторазово, використовуючи масив з отворами з елементами, пов'язаними між собою (вирішуючи це на рівні "контейнер" замість рівня "розподільник"):

struct FooNode
{
    explicit FooNode(const Foo& ielement): element(ielement), next(-1) {}

    // Stores a 'Foo'.
    Foo element;

    // Points to the next foo available; either the
    // next used foo or the next deleted foo. Can
    // use SoA and hoist this out if Foo doesn't 
    // have 32-bit alignment.
    int next;
};

struct Foos
{
    // Stores all the Foo nodes.
    vector<FooNode> nodes;

    // Points to the first used node.
    int first_node;

    // Points to the first free node.
    int free_node;

    Foos(): first_node(-1), free_node(-1)
    {
    }

    const FooNode& operator[](int n) const
    {
         return data[n];
    }

    void insert(const Foo& element)
    {
         int index = free_node;
         if (index != -1)
         {
              // If there's a free node available,
              // pop it from the free list, overwrite it,
              // and push it to the used list.
              free_node = data[index].next;
              data[index].next = first_node;
              data[index].element = element;
              first_node = index;
         }
         else
         {
              // If there's no free node available, add a 
              // new node and push it to the used list.
              FooNode new_node(element);
              new_node.next = first_node;
              first_node = data.size() - 1;
              data.push_back(new_node);
         }
    }

    void erase(int n)
    {
         // If the node being removed is the first used
         // node, pop it from the used list.
         if (first_node == n)
              first_node = data[n].next;

         // Push the node to the free list.
         data[n].next = free_node;
         free_node = n;
    }
};

Щось до цього ефекту: окремо пов'язаний список індексів із вільним списком. Посилання індексу дозволяють пропускати через видалені елементи, видаляти елементи в постійному часі, а також повертати / повторно використовувати / перезаписувати вільні елементи з вставкою постійного часу. Щоб повторити структуру, ви робите щось подібне:

for (int index = foos.first_node; index != -1; index = foos[index].next)
    // do something with foos[index]

введіть тут опис зображення

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

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

Foos(const Foos& other)
{
    for (int index = other.first_node; index != -1; index = other[index].next)
        insert(foos[index].element);
}

void Foos::swap(Foos& other)
{
     nodes.swap(other.nodes):
     std::swap(first_node, other.first_node);
     std::swap(free_node, other.free_node);
}

// ... then just copy and swap:
Foos(foos).swap(foos);

Тепер нова версія знову зручна в кеш-пам’яті. Інший метод - це зберігати в структурі окремий список індексів і періодично їх сортувати. Інша - використовувати біт, щоб вказати, які індекси використовуються. Це завжди дозволить вам переміщати біт в послідовному порядку (щоб це зробити ефективно, перевіряйте 64-бітні одночасно, наприклад, використовуючи FFS / FFZ). Біт-набір є найефективнішим і не нав'язливим, вимагає лише паралельного біта на один елемент, щоб вказати, які з них використовуються, а які видаляються, замість того, щоб вимагати 32-бітових nextіндексів, але найбільш трудомісткий, щоб записати добре (він не буде будьте швидкі для обходу, якщо ви перевіряєте один біт за один раз - вам потрібно FFS / FFZ, щоб знайти набір або скинути біт негайно серед 32+ бітів одночасно, щоб швидко визначити діапазони зайнятих індексів).

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

Чи варто створити будь-який пул пам’яті для динамічного розподілу, чи не потрібно це турбуватися? Що робити, якщо цільовою платформою є мобільні пристрої?

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

Що стосується того, що Джош згадував про GC, я не вивчав реалізацію GC C # настільки близько, як Java, але у GC-розподільників часто є початкове виділенняце дуже швидко, тому що використовується послідовний розподільник, який не може звільнити пам'ять із середини (майже як стек, ви не можете видалити речі з середини). Тоді вона окупає дорогі витрати, фактично дозволяючи видаляти окремі об’єкти в окремий потік, копіюючи пам'ять і очищаючи раніше виділену пам'ять у цілому (наприклад, знищуючи весь стек одразу, копіюючи дані на щось більше, як пов'язана структура), але оскільки це робиться в окремому потоці, воно не обов'язково так сильно затримує нитки програми. Однак це несе в собі дуже значну приховану вартість додаткового рівня непрямості та загальної втрати ЛОР після початкового циклу ГК. Це ще одна стратегія прискорити розподіл - здешевити її в потоці виклику, а потім виконати дорогу роботу в іншій. Для цього вам потрібно два рівні непрямості для посилання на ваші об’єкти замість одного, оскільки вони в кінцевому підсумку перетасуються в пам'яті між часом, який ви спочатку виділите, і після першого циклу.

Ще одна стратегія подібного типу, яку трохи простіше застосувати в C ++, - це просто не турбуватися звільняти об'єкти в основних темах. Просто зберігаючи додавання та додавання та додавання до кінця структури даних, яка не дозволяє видаляти речі з середини. Однак позначте ті речі, які потрібно зняти. Тоді окремий потік може піклуватися про дорогу роботу зі створення нової структури даних без вилучених елементів, а потім атомним чином поміняти нову на стару, наприклад, велика частина витрат на виділення та звільнення елементів може бути передана на окремий потік, якщо ви можете зробити припущення, що запит на видалення елемента не повинен бути задоволений негайно. Це не тільки робить звільнення дешевшим, що стосується ваших потоків, але робить розподіл дешевшим, оскільки ви можете використовувати набагато простішу і тупішу структуру даних, якій ніколи не доводиться обробляти випадки видалення з середини. Це як контейнер, який потребує лишеpush_backфункція для вставки, clearфункція для видалення всіх елементів та розміщення swapвмісту новим, компактним контейнером, виключаючи вилучені елементи; це все, що стосується мутування.

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