Управління пам'яттю для швидкого проходження повідомлень між потоками в C ++


9

Припустимо, є два потоки, які спілкуються, асинхронно надсилаючи повідомлення один одному. Кожен потік має якусь чергу повідомлень.

У мене питання дуже низький: Який очікується, що це найефективніший спосіб управління пам'яттю? Я можу придумати кілька рішень:

  1. Відправник створює об'єкт через new. Дзвінки приймача delete.
  2. Об'єднання пам’яті (для повернення пам’яті назад відправника)
  3. Збір сміття (наприклад, Boehm GC)
  4. (якщо об’єкти досить малі) скопіюйте за значенням, щоб уникнути виділення купи повністю

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

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

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

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

Відповіді:


9

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

Якщо ви можете передбачити верхню межу char buf[256], наприклад, практичну альтернативу, якщо ви не можете, яка викликає виділення лише у рідкісних випадках:

struct Message
{
    // Stores the message data.
    char buf[256];

    // Points to 'buf' if it fits, heap otherwise.
    char* data;
};

3

Це буде залежати від того, як ви реалізуєте черги.

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

Тоді об'єднання ресурсів може бути легко виконано, коли ви просто заміните нове і видаліть на AllocMessage<T>і freeMessage<T>. Моя пропозиція полягає в тому, щоб обмежити кількість можливих розмірів, які Tможуть мати, і округлити при розподілі бетону messages.

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


3

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

Вам все одно потрібно буде обробляти блокування між потоками, але продуктивність буде хорошою, оскільки не буде скопійовано пам'ять (лише сам об'єкт ptr, який крихітний).

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


2
Блокування зазвичай набагато більша проблема, ніж копіювання пам'яті. Просто кажу.
tdammers

Коли ви пишете unique_ptr, я думаю, ви маєте на увазі shared_ptr. Але хоча немає сумнівів, що використання розумного вказівника добре для управління ресурсами, це не змінює факту, що ви використовуєте певну форму розподілу пам’яті та розсилку. Я думаю, що це питання є більш низьким рівнем.
5gon12eder

3

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

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

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

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

    • Приймач перевіряє, чи є елемент для читання, порівнюючи покажчики, потім зчитує елемент, а потім збільшує покажчик зчитування.

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

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

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

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

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

Обидва підходи набагато швидші, ніж будь-який підхід на основі блокування, але для правильного їх виконання потрібна ретельна реалізація. І, звичайно, їм потрібна натурна апаратна атомність записів / навантажень; якщо ваша atomic<>реалізація використовує блокування внутрішньо, ви майже приречені.

Так само, якщо у вас є кілька читачів та / або авторів, ви дуже приречені: ви можете спробувати придумати схему без блокування, але в кращому випадку її буде складно реалізувати. Ці ситуації набагато простіше впоратися із замком. Однак, як тільки ви захопите замок, ви можете перестати турбуватися про new/ deleteпродуктивність.


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