Подтвердить что ты не робот

Есть ли преимущество в выборе любой петли как внешнего цикла?

Я расширяю существующую регистрационную библиотеку. Это система с двумя сторонами. Интерфейс - это то, где задачи записывают свои сообщения журнала, а бэкенд - это приложение, которое может подключать слушателей, в которые пересылаются эти сообщения различным приемникам. Бэкэнд раньше был одним проводным слушателем, теперь я расширяю его для гибкости. Код должен использоваться исключительно на встроенных устройствах, где высокая производительность (измеренная в количестве переадресованных байтов за миллисекунду) является очень важной задачей проектирования и реализации.

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

Учитывая это, я получаю N функции уведомления (слушатели) для отправки сообщений M в довольно классическую проблему N*M. Теперь у меня есть две возможности: я могу контактировать с сообщениями, а затем перебирать функции уведомлений, передавая сообщение каждому из них.

for(m in formatted_messages) 
  for(n in notification_functions)
    n(m);

void n(message)
{
    if( filter(message) )
      write(message);
}

Или я мог бы перебрать все функции уведомлений и передать им все сообщения, которые у меня есть:

for(n in notification_functions)
    n(formatted_messages);

void n(messages)
{
  for(m in messages)
    if( filter(m) )
      write(m);
}

Существуют ли какие-либо основные соображения, относительно которых дизайн более вероятен для обработки большего количества сообщений на единицу времени? (Обратите внимание, как этот вопрос определяет интерфейс прослушивателя. Это не вопрос микро-оптимизации, а один о том, как создать дизайн, который не мешает производительности. Я могу измерить только намного позже, и перепроектирование интерфейса прослушивателя будет дорогостоящим.)

Некоторые соображения, которые я уже сделал:

  • Те слушатели должны где-то писать сообщения, что довольно дорого, поэтому функция сама по себе может не быть слишком важной по производительности.
  • В 95% всех случаев будет только один слушатель.
4b9b3361

Ответ 1

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

Естественное преимущество второго интерфейса (то есть отправка последовательности сообщений каждому слушателю) заключается в том, что вы включаете возможную группировку в реализацию слушателя. Например, при записи на устройство слушатель может упаковать несколько сообщений в один write, тогда как если интерфейс принимает одно сообщение, то либо кэширующие слушатели (которые имеют память и стоимость процессора), либо должны выполнять несколько writes по одному на звонок.

Ответ 2

Есть ли какие-либо фундаментальные соображения относительно того, какая конструкция более вероятна для обработки более большого количества сообщений на единицу времени?

В общем, основные соображения с этим часто сводятся к двум основным вопросам.

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

  • Если вы планируете распараллеливать операцию, сохранение коллекции "больше" (в терминах count) во внешнем цикле позволяет эффективно распараллеливать внешний цикл и не вызывать чрезмерной подписки на потоки и т.д. Как правило, проще и чище распараллеливать алгоритм на внешнем уровне, поэтому проектирование циклов с потенциально большими параллельными "блоками" работы во внешнем цикле может упростить это, если это возможно позже.

Те слушатели должны где-то писать сообщения, что довольно дорого, поэтому вызов функций сам по себе может быть не слишком важным по производительности.

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

В 95% всех случаев будет только один слушатель.

Если это так, я бы, скорее всего, поставил цикл слушателя во внешнюю область, если вы не планируете распараллеливать эту операцию. Учитывая, что это будет выполняться в фоновом потоке на встроенном устройстве, распараллеливание маловероятно, поэтому наличие цикла прослушивателя в качестве внешнего цикла должно уменьшать общий счетчик команд (он фактически становится циклом над операциями M, а не M петлями над одна операция).

Ответ 3

Итак, здесь будут играть несколько факторов:

Насколько близко друг к другу находятся сообщения в кеше и сколько места они занимают? Если они относительно небольшие (несколько килобайт или менее) и близко друг к другу (например, не связанный список с памятью, выделенной на несколько секунд в системе, которая делает много другого распределения памяти).

Если они близки и малы, я считаю, что второй вариант более эффективен, так как сообщения будут предварительно загружены/кэшированы вместе, где вызывается все функции n для прослушивания и фильтрации (также предполагается, что есть МНОГИЕ функции, а не один, два или три) может вызвать больше "кэширования" предыдущих сообщений. Это также будет зависеть от того, насколько сложны функции слушателя и фильтра, конечно. Сколько работы они делают? Если каждая функция выполняет довольно много работы, вероятно, это не так важно, какой заказ вы это делаете, потому что это будет просто незначительно.

Ответ 4

Нет основополагающих причин, по которым один из них лучше, чем другой. Есть несколько очень незначительных различий в скорости, которые могут возникнуть в зависимости от того, как используется ваша библиотека. Я лично предпочел бы сначала перебирать слушателей, а сообщения - второй.

Я предполагаю, что тела обработчиков, как правило, довольно быстро. Вероятно, вы захотите перебрать прослушиватели в качестве внешнего цикла, чтобы повторять один и тот же код. Такие вещи, как прогноз косвенного вызова, будут работать намного лучше. Конечно, вы все хуже используете кеш данных, но, надеюсь, каждый буфер сообщений достаточно мал, чтобы легко вписываться в L1.

Почему бы не заставить слушателей принять const vector<message> & и заставить их выполнять свою собственную итерацию? Они могут делать любую буферизацию полезной и только делают одну дорогостоящую запись в конце.