Привабливі приклади користувацьких C ++-розподільників?


176

Які існують справді вагомі причини, щоб піти std::allocatorна користь користувацького рішення? Чи стикалися ви з будь-якими ситуаціями, коли це було абсолютно необхідне для коректності, продуктивності, масштабованості тощо? Будь-які справді розумні приклади?

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

Відповіді:


121

Як я вже згадую тут , я бачив, що призначений для розширення STL Intel TBB значно покращує продуктивність багатопотокового додатку, просто змінивши один

std::vector<T>

до

std::vector<T,tbb::scalable_allocator<T> >

(це швидкий і зручний спосіб переключення розподільника на використання чудових ниток приватних ниток TBB; див. сторінку 7 у цьому документі )


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

7
Оригінальне посилання тепер не існує, але CiteSeer має PDF: citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.71.8289
Арто Бендікен

1
Я маю запитати: Чи можете ви надійно перемістити такий вектор в іншу нитку? (Я здогадуюсь ні)
sellibitze

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

@ArtoBendiken: посилання на завантаження за вашим посиланням не видається дійсним.
einpoklum

81

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

EASTL - бібліотека стандартних шаблонів електронних мистецтв


14
+1 для посилання EASTL: "Серед розробників ігор найбільш принциповою слабкістю [STL] є дизайн std-алокаторів, і саме ця слабкість була найбільшим фактором, що сприяв створенню EASTL."
Наафф

65

Я працюю над mmap-розподільником, який дозволяє векторам використовувати пам'ять із файлу, відображеного на пам'ять. Метою є наявність векторів, які використовують сховище, що знаходиться безпосередньо у віртуальній пам'яті, відображеній mmap. Наша проблема полягає в тому, щоб покращити читання дійсно великих файлів (> 10 Гб) в пам'яті без накладних копій, тому мені потрібен цей спеціальний розподільник.

Поки в мене є скелет користувацького алокатора (який походить від std :: allocator), я думаю, що це хороша відправна точка для написання власних розподільників. Сміливо використовуйте цей фрагмент коду будь-яким способом:

#include <memory>
#include <stdio.h>

namespace mmap_allocator_namespace
{
        // See StackOverflow replies to this answer for important commentary about inheriting from std::allocator before replicating this code.
        template <typename T>
        class mmap_allocator: public std::allocator<T>
        {
public:
                typedef size_t size_type;
                typedef T* pointer;
                typedef const T* const_pointer;

                template<typename _Tp1>
                struct rebind
                {
                        typedef mmap_allocator<_Tp1> other;
                };

                pointer allocate(size_type n, const void *hint=0)
                {
                        fprintf(stderr, "Alloc %d bytes.\n", n*sizeof(T));
                        return std::allocator<T>::allocate(n, hint);
                }

                void deallocate(pointer p, size_type n)
                {
                        fprintf(stderr, "Dealloc %d bytes (%p).\n", n*sizeof(T), p);
                        return std::allocator<T>::deallocate(p, n);
                }

                mmap_allocator() throw(): std::allocator<T>() { fprintf(stderr, "Hello allocator!\n"); }
                mmap_allocator(const mmap_allocator &a) throw(): std::allocator<T>(a) { }
                template <class U>                    
                mmap_allocator(const mmap_allocator<U> &a) throw(): std::allocator<T>(a) { }
                ~mmap_allocator() throw() { }
        };
}

Щоб використати це, оголосіть контейнер STL наступним чином:

using namespace std;
using namespace mmap_allocator_namespace;

vector<int, mmap_allocator<int> > int_vec(1024, 0, mmap_allocator<int>());

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

Оновлення: Розподільник карт пам’яті тепер доступний на веб- сайті https://github.com/johannesthoma/mmap_allocator і є LGPL. Сміливо використовуйте його для своїх проектів.


17
Просто голова вгору, що походить від std :: allocator, насправді не є ідіоматичним способом написання алокаторів. Натомість слід поглянути на allocator_traits, що дозволяє забезпечити мінімальний функціонал, а клас ознак забезпечить решту. Зауважте, що STL завжди використовує ваш алокатор через allocator_traits, а не безпосередньо, тому вам не потрібно самостійно посилатися на allocator_traits. Немає великого стимулу для отримання std :: allocator (хоча цей код може бути корисною відправною точкою незалежно).
Нір Фрідман

25

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


21

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

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


3
Або, коли цей пул пам’яті є спільним.
Ентоні

9

Я не написав код C ++ за допомогою спеціального розподільника STL, але можу уявити веб-сервер, написаний на C ++, який використовує спеціальний розподільник для автоматичного видалення тимчасових даних, необхідних для відповіді на HTTP-запит. Спеціальний розподільник може звільнити всі тимчасові дані одразу після отримання відповіді.

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


5
Здається, перший приклад - це робота деструктора, а не розподільника.
Майкл Дорст

2
Якщо ви переживаєте за свою програму залежно від початкового вмісту пам’яті з купи, швидкий (тобто протягом ночі!) Пробіг у valgrind дасть вам знати так чи інакше.
cdyson37

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

8

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

Причина, через яку користувацьке розподілення через прискорювач може бути корисним при використанні прискорювачів:

  1. за допомогою спеціального розподілу час роботи прискорювача або драйвер повідомляються про блок пам'яті
  2. крім того, операційна система може переконатися, що виділений блок пам'яті заблокований сторінками (деякі називають цю закріплену пам'ять ), тобто підсистема віртуальної пам'яті операційної системи може не переміщувати або видаляти сторінку всередині або з пам'яті
  3. якщо 1. і 2. утримується і запитується передача даних між блокованою сторінкою блоком пам'яті та прискорювачем, час виконання може безпосередньо отримати доступ до даних у головній пам'яті, оскільки він знає, де знаходиться, і можна впевнитись, що операційна система не зробила цього перемістити / видалити його
  4. це зберігає одну копію пам’яті, яка відбуватиметься з пам’яттю, виділеною не зафіксованою сторінкою: дані повинні бути скопійовані в основну пам’ять до області зйомки, заблокованої сторінкою, з допомогою акселератора, може ініціалізувати передачу даних (через DMA )

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

7

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

Передумови: у нас є перевантаження для malloc, calloc, free, а також різні варіанти оператора new і delete, і лінкер із задоволенням змушує STL використовувати це для нас. Це дозволяє нам робити такі операції, як автоматичне об'єднання невеликих об’єктів, виявлення витоків, заливка аллока, безкоштовне заповнення, розподіл накладок із довідками, вирівнювання кеш-рядків для певних алоків та відкладене вільне.

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

Рішення: напишіть спеціальний алокатор, який використовує розширену купу, і використовуйте її лише у внутрішніх архітектурах відстеження витоку пам'яті ... Все інше за замовчуванням до нормальних нових / видалення перевантажень, які відстежують витік. Це дозволяє уникнути відстеження самого трекера (і надає трохи додаткових функцій упаковки, ми знаємо розмір вузлів трекера).

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


5

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


4

Одна істотна ситуація: Коли ви пишете код, який повинен працювати через межі модулів (EXE / DLL), важливо зберегти свої розподіли та видалення лише в одному модулі.

Де я стикався з цим, була архітектура плагінів для Windows. Важливо, щоб, наприклад, якщо ви передаєте рядок std :: через межу DLL, щоб будь-яке перерозподіл рядка відбувалося з купи, звідки вона походила, а не з купи DLL, яка може бути різною *.

* Це складніше, ніж це насправді, так як якщо б ви динамічно підключалися до CRT, це може працювати в будь-якому випадку. Але якщо кожна DLL має статичне посилання на CRT, ви відправляєтесь у світ болю, де постійно виникають помилки розподілу фантомів.


Якщо ви передаєте об'єкти через межі DLL, ви повинні використовувати налаштування DLL (/ MD (d)) з багатопотоковими каналами для обох сторін. C ++ не розроблявся з урахуванням підтримки модулів. Крім того, ви можете захистити все, що лежить за інтерфейсами COM, і використовувати CoTaskMemAlloc. Це найкращий спосіб використання плагін-інтерфейсів, які не прив’язані до конкретного компілятора, STL або постачальника.
gast128

Старі хлопці для цього правило: Не робіть цього. Не використовуйте типи STL в API DLL. І не перекладайте відповідальність за динамічну пам'ять через межі API DLL. Немає C ++ ABI - тому якщо ви ставитесь до кожної DLL як API API, ви уникаєте цілого класу потенційних проблем. За рахунок «с ++ краси», звичайно. Або як підказує інший коментар: Використовуйте COM. Просто звичайний C ++ - це погана ідея.
BitTickler

3

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

Один проект, над яким я працював, - це використовувати AVR-GCC на деяких мікросхемах з низькою потужністю. Нам довелося зберігати 8 послідовностей змінної довжини, але з відомим максимумом. Стандартна реалізація бібліотеки управління пам'яттюявляє собою тонку обгортку навколо malloc / free, яка відслідковує, куди розміщувати предмети, попередньо додаючи кожен виділений блок пам'яті з вказівником лише на кінець цього виділеного фрагмента пам'яті. Виділяючи новий фрагмент пам'яті, стандартний розподільник повинен пройти по кожному з фрагментів пам'яті, щоб знайти наступний блок, який буде доступний, де потрібний розмір пам'яті. На настільній платформі це було б дуже швидко для цих кількох предметів, але ви повинні мати на увазі, що деякі з цих мікроконтролерів дуже повільні та примітивні в порівнянні. Крім того, проблема фрагментації пам’яті була величезною проблемою, яка означала, що у нас дійсно не залишається іншого вибору, як використовувати інший підхід.

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

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


3

Обов’язкове посилання на розмову CppCon 2015 про Андрія Олександреску про розподільники:

https://www.youtube.com/watch?v=LIb3L4vKZ7U

Приємно те, що лише придумуючи їх, ви змушуєте думати про ідеї, як би ви ними користувалися :-)


2

Для спільної пам’яті важливо, щоб не тільки головка контейнера, а й дані, які вони містять, зберігалися у спільній пам'яті.

Розподільник Boost :: Interprocess - хороший приклад. Однак, як ви можете прочитати тут цього аллону недостатньо, щоб зробити всі контейнери STL спільною пам'яттю сумісною (Через різні зміщення карток у різних процесах, покажчики можуть "зламатися").


2

Десь тому я знайшов це рішення дуже корисним для мене: швидкий розподільник C ++ 11 для контейнерів STL . Це трохи прискорює контейнери STL як на VS2017 (~ 5x), так і на GCC (~ 7x). Це розподільник спеціального призначення на основі пулу пам'яті. Його можна використовувати з контейнерами STL лише завдяки механізму, про який ви просите.


1

Я особисто використовую Loki :: Allocator / SmallObject для оптимізації використання пам'яті для дрібних об'єктів - це показує хорошу ефективність та задоволення продуктивності, якщо вам доведеться працювати з помірною кількістю дійсно невеликих об'єктів (від 1 до 256 байт). Це може бути до ~ 30 разів ефективніше, ніж стандартне виділення C ++ new / delete, якщо ми говоримо про виділення помірних кількостей невеликих об'єктів різного розміру. Також є специфічне для VC рішення під назвою "QuickHeap", воно приносить найкращі результати (розподілити та розмістити операції просто прочитати та записати адресу блоку, що виділяється / повертається до купи, відповідно до 99. (9)% випадків - залежить від налаштувань та ініціалізації), але ціною помітної накладних витрат - для цього потрібні два покажчики на ступінь та один додатковий на кожен новий блок пам'яті. Це '

Проблема зі стандартною реалізацією C ++ new / delete полягає в тому, що зазвичай це просто обгортка для виділення C malloc / free, і це добре працює для великих блоків пам'яті, як 1024+ байтів. Він має помітні накладні витрати з точки зору продуктивності, а іноді й додаткової пам’яті, що використовується і для картографування. Так, у більшості випадків користувацькі алокатори реалізуються таким чином, щоб досягти максимальної продуктивності та / або мінімізувати кількість додаткової пам'яті, необхідної для виділення невеликих (≤1024 байт) об’єктів.


1

У графічному моделюванні я бачив власні алокатори, які використовуються для

  1. Обмеження вирівнювання, які std::allocatorбезпосередньо не підтримуються.
  2. Мінімізація фрагментації, використовуючи окремі пули для короткочасних (саме цей кадр) та довготривалих виділень.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.