Управление памятью для быстрой передачи сообщений между потоками в 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

Это будет зависеть от того, как вы реализуете очереди.

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

Затем, пул ресурсов может быть легко сделан, когда вы просто замените новый и удалите с AllocMessage<T>и freeMessage<T>. Мое предложение было бы ограничить количество потенциальных размеров Tи округлить при распределении бетона messages.

Прямая сборка мусора может работать, но это может вызвать длинные паузы, когда необходимо собрать большую часть, и будет (я думаю) работать немного хуже, чем new / delete.

чокнутый урод
источник
3

Если это в C ++, просто используйте один из умных указателей - unique_ptr будет работать хорошо для вас, так как он не удалит базовый объект, пока никто не будет иметь дескриптор на нем. Вы передаете объект ptr получателю по значению, и вам никогда не нужно беспокоиться о том, какой поток должен его удалить (в тех случаях, когда получатель не получает объект).

Вам все равно придется обрабатывать блокировку между потоками, но производительность будет хорошей, поскольку не копируется память (только сам объект ptr, который крошечный).

Выделение памяти в куче - не самая быстрая вещь, поэтому объединение используется для того, чтобы сделать это намного быстрее. Вы просто берете следующий блок из кучи предварительно заданного размера в пуле, поэтому просто используйте для этого существующую библиотеку .

gbjbaanb
источник
2
Блокировка обычно гораздо более серьезная проблема, чем копирование памяти. Просто говорю.
tdammers
Когда ты пишешь unique_ptr, я думаю, ты имеешь в виду shared_ptr. Но хотя нет сомнений в том, что использование умного указателя хорошо для управления ресурсами, это не меняет того факта, что вы используете какую-то форму выделения и освобождения памяти. Я думаю, что этот вопрос более низкого уровня.
5gon12eder
3

Наибольшее снижение производительности при передаче объекта из одного потока в другой - это накладные расходы на захват блокировки. Это порядка нескольких микросекунд, что значительно больше, чем среднее время, которое пара new/ deleteзанимает (порядка ста наносекунд). Разумные newреализации стараются избегать блокировок практически любой ценой, чтобы избежать снижения производительности.

Тем не менее, вы хотите убедиться, что вам не нужно захватывать блокировки при передаче объектов из одного потока в другой. Я знаю два основных метода для достижения этой цели. Оба работают только однонаправленно между одним отправителем и одним получателем:

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

    • Отправитель сначала проверяет, есть ли место для добавления элемента, сравнивая указатели, затем добавляет элемент, а затем увеличивает указатель записи.

    • Получатель проверяет, есть ли элемент для чтения, сравнивая указатели, затем читает элемент, а затем увеличивает указатель чтения.

    Указатели должны быть атомарными, поскольку они разделяются между потоками. Однако каждый указатель изменяется только одним потоком, другому нужен только доступ для чтения к указателю. Элементы в буфере могут быть самими указателями, что позволяет вам легко изменить размер вашего кольцевого буфера до размера, который не будет блокировать отправителя.

  2. Используйте связанный список, который всегда содержит хотя бы один элемент. Получатель имеет указатель на первый элемент, отправитель имеет указатель на последний элемент. Эти указатели не являются общими.

    • Отправитель создает новый узел для связанного списка, устанавливая его nextуказатель на nullptr. Затем он обновляет nextуказатель последнего элемента, чтобы он указывал на новый элемент. Наконец, он сохраняет новый элемент в своем собственном указателе.

    • Получатель следит за nextуказателем первого элемента, чтобы увидеть, есть ли новые доступные данные. Если это так, он удаляет старый первый элемент, продвигает свой собственный указатель для указания на текущий элемент и начинает его обработку.

    В этой настройке nextуказатели должны быть атомарными, и отправитель должен быть уверен, что не разыменовывает второй последний элемент после того, как он установил свой nextуказатель. Преимущество, конечно, в том, что отправителю никогда не придется блокировать.

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

Аналогичным образом, если у вас есть несколько читателей и / или писателей, вы в значительной степени обречены: вы можете попытаться придумать схему без блокировки, но в лучшем случае ее будет сложно реализовать. С этими ситуациями гораздо легче справиться с помощью замка. Однако, как только вы захватить замок, вы можете перестать беспокоиться о new/ deleteпроизводительности.

cmaster - восстановить монику
источник
+1 Я должен проверить это решение кольцевого буфера как альтернативу параллельным очередям, использующим циклы CAS. Это звучит очень многообещающе.