среда, 11 февраля 2015 г.

[prog.c++11] Что нового будет в SObjectizer-5.5.3

Сейчас идет плотная работа по подготовке релиза SO-5.5.3. Релиз должен состоятся со дня на день. Буквально как только, так сразу :) Пока же можно рассказать, что нового принесет версия 5.5.3.

По большому счету ничего серьезного. Но пойдем по порядку, от простого к сложному.

Маленькое, но приятное дополнение: функция so_5::rt::create_child_coop(), которая упрощает создание дочерних коопераций. Раньше нужно было писать

void some_parent_agent::evt_do_some_complex_task()
{
  // A child cooperation must be created for this task.
  auto child = so_environment().create_coop( so_5::autoname );
  child->set_parent_coop_name( so_coop_name() );
  ...
}

Сейчас можно написать вот так:

void some_parent_agent::evt_do_some_complex_task()
{
  // A child cooperation must be created for this task.
  auto child = so_5::rt::create_child_coop( *this, so_5::autoname );
  ...
}

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


Второе нововведение более серьезное. Это выплата старого технического долга, который появился, когда в SObjectizer добавили ad-hoc-агентов и direct_mbox-ы.

Дело в том, что у каждого агента в SObjectizer есть direct_mbox, в том числе и у ad-hoc-агентов. Но direct_mbox-ы доступны через API класса agent_t, а для ad-hoc-агентов нет доступа к этому API. Получалось, что ad-hoc-агент есть, его direct_mbox так же есть, а вот работать с этим direct_mbox-ом нельзя. Эта проблема была исправлена.

Метод agent_coop_t::define_agent(), который создает ad-hoc-агентов, возвращает proxy-объект. Настройка ad-hoc-агента выполняется через этот proxy-объект. Теперь у proxy появился метод direct_mbox(), который возвращает ссылку на direct_mbox спрятанного за proxy агента.

Что позволяет писать, например, вот так:

auto coop = env.create_coop( so_5::autoname );

// A proxy for the first ad-hoc agent.
auto first = coop.define_agent();
// An proxy for the second ad-hoc agent.
auto second_mbox = env.define_agent();

// At this point two agents are created but no one of them have any useful events.

// Definition for the first agent.
first
   .on_start(
      [second]() { so_5::send< msg_initiate_work >(second.direct_mbox()); } )
   .event< msg_work_done >( first.direct_mbox(),
      [second]() { so_5::send< msg_do_more_work >(second.direct_mbox()); } );

// Definition for the second agent.
second
   .event< msg_initiate_work >( second.direct_mbox(),
      [first]() {
         ... // Some processing.
         // Sending acknowledgement.
         so_5::send< msg_work_done >(first.direct_mbox());
      } )
   .event< msg_do_more_work >( second.direct_mbox(),
      [first]() {
         ... // Some processing.
         // Sending acknowledgement.
         so_5::send< msg_work_done >(first.direct_mbox());
      } );

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


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

SObjectizer принципиально отличается от аналогов вроде Akka и CAF тем, что диспетчеризация сообщений по обработчикам осуществляется не пользователем (который, например, в Akka пишет match по типам входящего сообщения), а самим SObjectizer-ом. Для этого SObjectizer хранит подписки агента в специальном хранилище, где триплету (mbox,msg_type,state) поставлен в соответствие обработчик сообщения типа msg_type из почтового ящика mbox для состояния state. При диспетчеризации сообщения SObjectizer формирует для очередного сообщения триплет (mbox,msg_type,current_state) и ищет подходящий обработчик.

Соответственно, скорость поиска в хранилище подписок сильно зависит от того, какая структура используется для хранения подписок. А структура зависит от особенностей конкретной ситуации. Так, если агент хранит всего две-три подписки (что обычное дело для тестов и демонстрационных примеров, а так же для большого количества мелких агентов в реальной жизни), то самым оптимальным по памяти и скорости будет хранение подписок в простом std::vector, с элементарным и тупым линейным поиском. На современных процессорах, где промах мимо кэша очень дорог, тогда как последовательный проход по уже закэшированным данным дешев, vector-based хранилище находится вне конкуренции по скорости поиска даже когда у агента десяток подписок.

Если же у агента от десятка до сотни подписок, то линейный поиск по std::vector уже не эффективен. В этом случае std::map показывает лучшие результаты.

Если же у агента подписки идут на сотни, а то и на тысячи штук (что вряд ли можно сделать вручную, зато запросто может получиться, если агент генерируется автоматически), то и std::map не самое эффективное решение. Гораздо лучше себя ведет хранилище на основе std::unordered_map (т.е. на основе hash_table).

Так вот, в SObjectizer-5.5.3 появилась возможность указать, какой тип хранилища должен использоваться агентом. Сделать это можно передав в конструктор базового класса agent_t дополнительный параметр: объект agent_tuning_options_t. Например, вот так:

using namespace so_5::rt;

class my_agent : public agent_t
{
public :
  my_agent( environment_t & env )
    : agent_t( env,
        tuning_options().subscription_storage_factory(
          vector_based_subscription_storage(4) ) )
  {}
  ...
};

В данном случае для агента будет задано vector-based хранилище, с начальным размером вектора подписок в четыре элемента (т.е. пока количество подписок не превысит четырех штук, этот вектор расти не будет).

Всего в 5.5.3 реализовано четыре типа хранилищ подписок:

  • vector-based, в основе которого лежит std::vector, растущий по мере добавления подписок. Оптимален для количества подписок до десятка;
  • map-based, в основе которого лежит std::map. Оптимален когда количество подписок не превышает одной-двух сотен;
  • tash-table-based, в основе которого std::unordered_map. Вариант для количества подписок свыше нескольких сотен штук;
  • adaptive. Хранилище, которое, на самом деле состоит из двух: маленького и большого. Пока количество подписок не первысило заданный порог, они хранятся в маленьком хранилище. Как только превысило -- подписки перемещаются в большое хранилище. Если затем количество подписок снижается ниже порога, происходит обратное переключение с большого хранилища на маленькое.

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

so_5::rt::adaptive_subscription_storage_factory(
   // First threshold for switching from the first storage to the second.
   10,
   // First storage -- simple vector-based.
   so_5::rt::vector_based_subscription_storage_factory(10),
   // There will be another adaptive storage.
   so_5::rt::adaptive_subscription_storage_factory(
      // Second threshold for switching from the second storage to the third.
      100,
      // Second storage.
      so_5::rt::map_based_subscription_storage_factory(),
      // Third storage.
      so_5::rt::hash_table_based_subscription_storage_factory() ) );

Здесь до 10 подписок будет использоваться vector-based хранилище, от 10 до 100 -- map-based, свыше 100 -- hash-table-based.

Нужно ли пользователю заострять свое внимание на типах хранилищ подписок? Обычно нет. Однако, если разработчик ударяется в экстрим, например, создает десять миллионов мелких агентов, обрабатыващих 3 сообщения каждый, или же работает с агентами с тысячами подписок, то у него появляются средства для более точной настройки агентов под свои специфические нужды. Все-таки, когда речь заходит о большом количестве агентов и/или подписок, то и память внезапно переходит в разряд дефицитного ресурса. Да и быстродействия хочется побольше :)


Upd. Отмечу еще пару вещей, про которые забыл сказать сразу.

Экспериментальная поддержка CMake теперь расширена на весь SObjectizer, т.е. посредством CMake можно собрать и библиотеку, и примеры, и тесты. Большое спасибо Алексею Сырникову за проделанную работу!

В состав SObjectizer включено несколько новых примеров: collector_many_performers, collector_performer_pair, ping_pong_with_owner, simple_message_deadline.


В завершение хочется сказать, что с момента выхода версии 5.5.2 прошло весьма прилично времени, а нововведений в 5.5.3 не так уж, чтобы много, хотелось бы большего. Но это не потому, что работа над SObjectizer была заброшена. Как раз за прошедшее время было предпринято много усилий, которые не дали прямого осязаемого выхлопа, но зато позволили лучше понять, куда идти не следует, а куда нужно внимательно посмотреть. Т.е. это было время экспериментов и поиска. А эксперименты и поиск -- это такое занятие, в котором отрицательный результат не менее, если не более, вероятен, чем положительный.

Тем не менее, надеюсь, что работа над следующей версией (будь то 5.5.4 или 5.6.0) пойдет более предсказуемо и продуктивно.

PS. Первый публичный релиз SO-5 в виде версии 5.1.0 состоялся в мае 2013-го. Т.е. без малого два года назад. Оглядываясь назад ловлю себя на мысли, что SO-5.5 -- это уже заметно другой инструмент, нежели SO-5.1. Что не может радовать. Думаю, что дальше будет еще лучше.

Комментариев нет: