Ефективний менеджер пам'яті для однотипних елементів


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

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

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

Будемо зберігати все, що напишемо далі, в спеціальному класі, який і буде займатися розподілом пам'яті.

Однак, де будуть зберігатися блоки структури myMemBlock? Застосуємо невелику хитрість - будемо зберігати їх у вигляді масиву. Для цього динамічно виділимо масив з N елементів, і будемо користуватися ним в якості ресурсу вільних блоків пам'яті. Заведемо структуру для одного з таких масивів.

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

Клас myManager прийме наступний вигляд.

Настав час для написання однієї з найголовніших функцій - виділення блоку. Як буде відбуватися виділення? Для початку, ми перевіряємо наявність вільних елементів в списку FreeBlocks, і при наявності таких, просто видаємо перший елемент списку і виключаємо його. При відсутності вільних елементів, ми розподіляємо черговий елемент списку Arrays у вигляді структури myBigArray і заносимо всі новостворені елементи масиву Array [N] в список вільних блоків. Після чого отримуємо вихідну ситуацію для першого випадку.

Чергова функція, потрібна для роботи нормального менеджера пам'яті, буде описана далі. Необхідно не тільки виділяти блоки, але і звільняти їх.

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

Отже, чергова редакція класу myManager.

На малюнку показано розташування об'єктів в разі N = 6 і кількості масивів, що дорівнює чотирьом. Світлим кольором виділені вільні блоки, а темним - зайняті.

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

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

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

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

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

Використання менеджера можна представити таким чином. Створимо глобальні об'єкти для кожного з класів елементів і напишемо найпростіші макроси. наприклад:

Схожі статті