понедельник, 19 мая 2014 г.

[prog.c++11] Еще на тему использования функционального стиля в современном C++

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

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

Но стандарт C++11 уже несколько лет как утвержден. И теперь у C++ разработчиков есть выбор: делать ли такие заявки посредством объектных иерархий или же задействовать std::function (а так же лямбда-функции с замыканиями). Но, если делать выбор в пользу std::function, то не придется ли платить за это больше, чем за объекты и std::shared_ptr?

Ради проверки этого я написал для себя небольшой тест, исходный текст которого упрятан под кат. Проверял под VC++11, VC++12 и GCC 4.8.2 под 64-х битовым Windows 7.

Upd. Ув.тов.Андрей Валяев обнаружил ошибку в моем коде. После ее исправления выяснилось, что код на std::function где-то на 35-40% медленнее кода с std::shared_ptr и объектами.

Upd2. Посредством небольшого трюка можно разогнать очередь std::function очень сильно и std::function начнут обгонять std::shared_ptr.

Небольшой дисклаймер: в приведенном ниже коде можно было oo_demand_type_N сделать компактнее. Просто так больше похоже на то, что будет в реальной жизни, когда каждый наследник demand_t будет иметь кучу собственного уникального кода.

И еще одно небольшое дополнение напоследок: думаю, ситуация, когда заявка будет иметь всего один метод handle, т.е. когда объект-заявка может быть представлена функциональным объектом один-в-один, является частным случаем. На мой взгляд, велика вероятность, что у объекта-заявки будет несколько виртуальных методов. Если не сразу, то в процессе эволюции кода. И если иерархия конкретных заявок изначально представлена в виде объектной иерархии, то произвести модификацию кода с объектами будет проще, чем с std::function.

Суть теста: есть разные типы заявок, которые делают разные действия. Есть очередь заявок, есть генератор заявок. Для простоты сделано так, что заявок всего три типа и каждый тип заявки обращается к генератору с тем, чтобы генератор сделал заявку следующего типа. Т.е. заявка типа 1 приводит к генерации заявки типа 2, та к генерации заявки типа 3, а та -- к генерации заявки типа 1. И так по кругу, пока не будет проведено достаточное количество итераций.

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

#include <iostream>
#include <queue>
#include <memory>
#include <functional>
#include <ctime>

const size_t ITERATIONS = 100000000;

//
// Functional demands
//
typedef std::function< void(void) > fn_demand_t;

typedef std::queue< fn_demand_t > fn_demand_queue_t;

class fn_demand_generator_t
   {
   public :
      void
      on_type_1( fn_demand_queue_t & queue )
         {
            queue.push(
                  [this, &queue] { this->on_type_2( queue ); } );
         }

      void
      on_type_2( fn_demand_queue_t & queue )
         {
            queue.push(
                  [this, &queue] { this->on_type_3( queue ); } );
         }

      void
      on_type_3( fn_demand_queue_t & queue )
         {
            queue.push(
                  [this, &queue] { this->on_type_1( queue ); } );
         }
   };

void
fn_test()
   {
      fn_demand_queue_t queue;
      fn_demand_generator_t generator;

      generator.on_type_1( queue );
      forsize_t i = 0; i != ITERATIONS; ++i )
         {
            auto d = queue.front();
            queue.pop();

            d();
         }
   }

//
// Object-oriented demands.
//

class oo_demand_t
   {
   public :
      virtual ~oo_demand_t()
         {}

      virtual void
      handle() = 0;
   };

typedef std::shared_ptr< oo_demand_t > oo_demand_shptr_t;

typedef std::queue< oo_demand_shptr_t > oo_demand_queue_t;

class oo_demand_generator_t
   {
   public :
      void
      on_type_1( oo_demand_queue_t & queue );

      void
      on_type_2( oo_demand_queue_t & queue );

      void
      on_type_3( oo_demand_queue_t & queue );
   };

class oo_demand_type_1 : public oo_demand_t
   {
   private :
      oo_demand_generator_t & m_generator;
      oo_demand_queue_t & m_queue;

   public :
      oo_demand_type_1(
         oo_demand_generator_t & generator,
         oo_demand_queue_t & queue )
         :  m_generator( generator )
         ,  m_queue( queue )
         {}

      virtual void
      handle()
         {
            m_generator.on_type_1( m_queue );
         }
   };

class oo_demand_type_2 : public oo_demand_t
   {
   private :
      oo_demand_generator_t & m_generator;
      oo_demand_queue_t & m_queue;

   public :
      oo_demand_type_2(
         oo_demand_generator_t & generator,
         oo_demand_queue_t & queue )
         :  m_generator( generator )
         ,  m_queue( queue )
         {}

      virtual void
      handle()
         {
            m_generator.on_type_2( m_queue );
         }
   };

class oo_demand_type_3 : public oo_demand_t
   {
   private :
      oo_demand_generator_t & m_generator;
      oo_demand_queue_t & m_queue;

   public :
      oo_demand_type_3(
         oo_demand_generator_t & generator,
         oo_demand_queue_t & queue )
         :  m_generator( generator )
         ,  m_queue( queue )
         {}

      virtual void
      handle()
         {
            m_generator.on_type_3( m_queue );
         }
   };

inline void
oo_demand_generator_t::on_type_1( oo_demand_queue_t & queue )
   {
      queue.push( std::make_shared< oo_demand_type_2 >( *this, queue ) );
   }

inline void
oo_demand_generator_t::on_type_2( oo_demand_queue_t & queue )
   {
      queue.push( std::make_shared< oo_demand_type_3 >( *this, queue ) );
   }

inline void
oo_demand_generator_t::on_type_3( oo_demand_queue_t & queue )
   {
      queue.push( std::make_shared< oo_demand_type_1 >( *this, queue ) );
   }

void
oo_test()
   {
      oo_demand_queue_t queue;
      oo_demand_generator_t generator;

      generator.on_type_1( queue );
      forsize_t i = 0; i != ITERATIONS; ++i )
         {
            auto d = queue.front();
            queue.pop();

            d->handle();
         }
   }

//
// Benchmarking
//

templateclass TEST_CASE >
void
run_and_measure(
   const char * test_name,
   TEST_CASE test_case )
   {
      const std::clock_t begin = std::clock();

      test_case();

      const std::clock_t end = std::clock();

      const double duration = std::difftime(end,begin) / CLOCKS_PER_SEC;
      std::cout << test_name << ": " << duration << "s" << std::endl;
   }

int
main()
   {
      run_and_measure( "fn_demands", fn_test );
      run_and_measure( "oo_demands", oo_test );
   }

Еще на тему использования функциональных объектов в C++11: std::function и лямбды дают возможность к применению ФПшных приемов.

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