SObjectizer v.4.4 beta6 Notes

Author: Евгений Охотников
Contact: eao197 at intervale dot ru; eao197 at yahoo dot com
Date: 2008.09.17

Contents

Введение

Этот рассказ продолжает серию описаний, сопровождавших выходы предшествующих beta-версий SObjectizer 4.4 (beta3 и beta5).

В очередной, шестой, beta-версии удалось воплотить в жизнь несколько идей, которые сформировались на основании опыта использования SObjectizer 4.4.0-beta5 в ряде проектов компании Интервэйл. В результате чего была существенно увеличена производительность, а также добавлена возможность использования нескольких реакторов в SObjectizer приложениях.

Повышение производительности

Вот результаты тестов, замеряющих производительность SObjectizer 4.4.beta5 и 4.4.beta6 (Intel Core2Duo 2GHz, 2Gb Ram, WinXP SP2, Visual C++ 7.1 SP1):

Название теста beta5 beta6
chameneos 313K 492K

customer_ring (N=10K, M=100)

10000 агентов в кольце, 100 токенов

215K 415K

customer_ring (N=10K, M=100, AG=2)

две активные группы

200K 170K
customer_ring (N=10K, M=100, AG=4) 173K 251K
customer_ring (N=10K, M=100, AG=8) 167K 306K
customer_ring (N=10K, M=100, AG=16) 166K 351K

customer_ring (N=1K, M=100, AO)

1000 активных агентов

117K 458K
insend_and_process_msg 304K 492K

ping_pong (l=16, r=10K, ao)

два активных агента обмениваются 10000 сообщений размером 16 байт

109K 134K
process_state_changes 110K/160K 160K/182K
send_and_process_msg 377K/800K 709K/917K
send_msg 598K 1280K

Пояснение 1. Значения указываются в тысячах сообщений/событий в секунду. Т.е. значение 315K -- это 315 тысяч сообщений в секунду.

Пояснение 2. В тестах process_state_changes и send_and_process_msg замеряются две фазы -- отсылка сообщений и обработка сообщений. Первое из приведенных чисел обозначает скорость работы на первой фазе, второе -- на второй фазе. Так, значения 160K/182K указывают, что скорость отсылки сообщений составляла 160 тысяч сообщений в секунду, а скорость обработки сообщений -- 182 тысячи событий в секунду.

Пояснение 3. В тесте customer_ring с двумя активными группами показатели beta6 хуже показателей beta5 только на указанной выше машине с Core2Duo. На одноядерных и 4-хядерных машинах, на которых также были проведены соответствующие замеры, beta6 была стабильно быстрее beta5. Видимо, это тот случай, когда два ядра на тесте customer_ring настолько быстро обрабатывают события тестовых агентов, что большую часть времени ядра процессора проводят на синхронизации доступа к каким-то разделяемым структурам SObjectizer или run-time библиотеки C++. Устранение этого "бутылочного горлышка" будет задачей следующих бета-версий SObjectizer.

Использование нескольких реакторов

В beta5 был совершен окончательный переход на использование средств библиотеки ACE в реализации транспортных агентов. За диспетчеризацию событий ввода-вывода в beta5 отвечал единственный объект ACE_Select_Reactor, который являлся реактором по умолчанию. Это означало, что если в приложении создается много транспортных каналов, то они все обрабатываются с помощью этого реактора. Единственным способом смены реактора была замена реактора по умолчанию с помощью метода ACE_Reactor::instance.

В beta6 в SObjectizer было добавлено понятие реестра реакторов. Программист может заполнить реестр тем количеством реакторов, которое ему необходимо. При старте SObjectizer Run-Time все реакторы из этого реестра будут запущены, а при завершении работы SObjectizer Run-Time -- остановлены.

Каждый реактор в реестре идентифицируется собственным уникальным именем. Это дает возможность найти нужный реактор в реестре и привязать транспортного агента к данному реактору:

so_4::rt::comm::a_sop_incoming_channel_processor_t a_channel(
  "a_channel",
  // Для задания реактора нужно использовать функцию
  // create_acceptor_controller со всеми параметрами.
  so_4::transport_layer::socket::create_acceptor_controller(
      so_4::transport_layer::socket::acceptor_params( ip_address ),
      so_4::transport_layer::channel_params_t(),
      so_4::transport_layer::socket::option_setter_auto_ptr_t(),
      // Вот и реактор, на котором предстоит работать каналу.
      so_4::ace::reactor_registry().find( "a_channel_reactor" ) ) );

В SObjectizer 4.4 beta6 предоставляются готовые реализации реакторов ACE_Select_Reactor и ACE_TP_Reactor (аналог Select_Reactor-а, работающий на пуле потоков), которые могут использоваться с реестром реакторов.

Некоторые детали

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

Использование hash-таблиц вместо std::map

Ранее в SObjectizer для организации различных словарей использовался только тип std::map. В beta6 в нескольких местах (глобальный словарь агентов, список активных агентов, список сообщений у агента) вместо std::map теперь используется hash-таблица.

В качестве реализации hash-таблицы взят класс ACE_Hash_Map_Manager_Ex из состава ACE. Это не самая быстрая и удобная в использовании реализация, но зато она не увеличивает списка зависимостей SObjectizer. Со временем можно будет перейти на использование unordered_map из TR1.

Введение понятия dispatcher_binding

Предшествующие версии SObjectizer использовали централизованную иерархическую схему передачи заявок на выполнение событий рабочим нитям диспетчера. SObjectizer Run-Time знал только одного диспетчера, который был ему передан в so_4::api::start. Этому диспетчеру передавались все заявки, и он должен был разбираться с тем, на какую из рабочих нитей заявка должна быть отправлена.

В случае цепочки диспетчеров (наиболее распространенная схема: верхний диспетчер с активными группами, ему подчиняется диспетчер с активными агентами) это приводило к нескольким процедурам диспетчеризации заявки:

  • сначала в верхнем диспетчере (диспетчер с активными группами проверяет принадлежность агента какой-нибудь активной группе);
  • затем в подчиненном диспетчере (диспетчер с активными агентами проверяет, является ли агент активным агентом).

Подобные проверки требовали захвата mutex-ов, защищавших словари диспетчеров, и поиска имени агента в словарях. И так при диспетчеризации каждого события.

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

После регистрации каждый агент получает указатель на свой dispatcher_binding. При диспетчеризации заявки этот указатель используется SObjectizer-ом для передачи заявки непосредственно рабочей нити агента. Что устранило необходимость дополнительной синхронизации и поиска имени агента по словарям диспетчеров.

Изменение типа аргументов send_msg с std::string на string_piece

Изначально функции send_msg получали два типа аргументов: const char * и const std::string &. Но в конце концов, управление передавалось функции send_msg, с аргументами типа const std::string &. Это означало, что в случае, например, вызова:

so_4::api::send_msg( "some_agent_name", "msg_some_message" );

Для аргументов создавалось два объекта std::string, что могло сопровождаться динамическим выделением памяти, если конкретная реализация std::string не поддерживала оптимизацию для маленьких строк.

В beta6 вместо аргументов const char * и const std::string & используется аргументы типа string_piece_t. Этот тип является маленькой структурой, которая содержит только указатель на начало строки и ее длину. Создание таких структур занимает очень мало времени (самые большие накладные расходы в случае const char * -- это обращение к std::strlen).

Изменение места вызова message checker-ов

В предшествующих версиях SObjectizer функция проверки корректности сообщения (т.н. message checker) вызывался внутри send_msg перед генерацией событий. Поскольку message checker мог выполнять совершенно любые действия, то перед вызовом message checker-а send_msg выполнял разблокирование ядра SObjectizer Run-Time, а затем блокирование ядра для генерации заявок. Эта пара из разблокирования/блокирования ядра при вызове send_msg ощутимо сказывалось на производительности операции send_msg.

В beta6 вызов message checker-а отложен до момента вызова обработчика события на контексте рабочей нити. Что позволило отказаться от лишней разблокировки/блокировки ядра в send_msg. А это, в свою очередь, существенно подняло скорость работы send_msg.

Такое перемещение места вызова message checker-а, однако, привело к двум важным следствиям:

  • количество вызовов message checker-ов увеличилось. Если раньше на каждый экземпляр сообщения message checker вызывался всего один раз, то сейчас он вызывается для каждого события, сгенерированного сообщением;
  • код message checker-а должен быть thread safe, чтобы допускать параллельную работу сразу на нескольких рабочих нитях.

Ни одно из данных следствий не имело серьезных последствий на практике, так как все имеющиеся message checker-ы оказались реентерабельными функциями без побочных эффектов.

Использование векторов заявок вместо std::map

При генерации событий, порождённых сообщением, нужно выделять группы заявок для одного агента, имеющих одинаковый приоритет. Данная группа должна быть преобразована в одну заявку-композит: только одна составляющая этого композита должна быть разрешена к обработке в текущем состоянии агента.

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

В beta6 вместо std::map применяется простой вектор заявок. После формирования всех заявок этот вектор сортируется так, чтобы заявки для одного агента с одинаковым приоритетом оказались по соседству. Что позволяет затем одним проходом по результирующему вектору сформировать необходимые заявки-композиты (посредством объединения соседних элементов).

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

Уменьшение числа atomic_op при диспетчеризации сообщений

В beta6 проведена еще одна ревизия операций инкремента/декремента количества ссылок на объекты event_data_t. Теперь эти операции не выполняются, если заявка перемещается между внутренними очередями SObjectizer. Что существенно (иногда до 2-х раз) сократило количество выполняемых atomic_op при диспетчеризации сообщения.

Обновление ObjESSty

Для сериализации данных в SOP протоколе используется библиотека ObjESSty. В beta6 выполнен переход на ObjESSty 1.5, в которой серьезно переработан механизм операций сериализации/десериализации данных. В автономных тестах SObjectizer прирост производительности в этой части не столь заметен, зато в больших приложениях, разработанных на основе SObjectizer, активно передающих сложные структуры данных по SOP-каналам, отмечается прирост производительности в районе 5-10%.

Благодарности

С удовольствием выражаю благодарность:

Вновь хочу поблагодарить всех, кто скачивал SObjectizer и присылал свои отзывы и замечания.

Также хочется высказать отдельную благодарность клиентам нашей компании, которые каждый год удваивают нагрузку на наши системы. Что дает очень серьезный стимул, желание и потребность совершенствования SObjectizer и разработанных с его использованием продуктов.

Hosted by uCoz