понедельник, 7 июля 2014 г.

[prog.c++] О скорости и стоимости ACE_RW_Thread_Mutex

Начал работать над SObjectizer-5.4.0. В процессе переделок попробовал выбросить пул мутексов для того, чтобы каждый ресурс, которому нужна защита от параллельного доступа, создавал себе собственный мутекс. При прогоне набора unit-тестов выяснилось, что один из них стал работать невообразимо долго: вместо 1.8 секунды теперь время его работы составляло 1 минуту и 38 секунд. Отличительной особенностью теста являлось то, что в процессе работы создавалось и уничтожалось порядка 87K агентов, с каждым из которых было связано два мутекса. Ранее ссылки на эти мутексы выделялись из небольшого пула (т.е. несколько агентов использовали один общий мутекс), а сейчас стали персональной собственностью каждого агента.

Собака оказалась зарыта в ACE_RW_Thread_Mutex-е, который является реализацией идиомы single-writer/multi-reader lock. Мало того, что размер ACE_RW_Thread_Mutex составляет почти 300 байт, так еще, похоже, он выделяет для себя какие-то объекты ядра Windows (полагаю, создают Event-ы и получат от системы HANDLE для них). Думаю, что именно с использованием ресурсов Windows и связанно такое увеличение времени работы при создании в приложении нескольких десятков тысяч объектов ACE_RW_Thread_Mutex. Эту же гипотезу подтверждает и простейший автономный тест, который в цикле создает 100K объектов ACE_RW_Thread_Mutex -- его время работы на порядки больше, чем аналогичного теста, создающего такое же количество объектов std::mutex.

Печальная новость, т.к. стандартный для C++ std::shared_mutex появится только в C++14.

Сам же ACE_RW_Thread_Mutex работает весьма шустро. На тех задачах, где он нам нужен, т.е. на одновременном доступе к общему ресурсу на чтение, он оказывается от 1.5 до 2 раз быстрее, чем std::mutex (по результатам нескольких тестов, имитирующих реальную работу, тесты проводились на 4-х одновременно работающих потоках на двух ядрах с гипертрейдингом, т.е. на четырех аппаратных потоках).

Под катом исходный код одного из тестов. Там в качестве альтернативы ACE_RW_Thread_Mutex был проверен класс shared_mutex от автора вот этой большой статьи. Данная реализация у меня работала раза в два медленнее, чем обычный std::mutex и где-то в четыре раза медленнее, чем ACE_RW_Thread_Mutex. Я это могу объяснить только тем, что упрятанный внутри shared_mutex реальный std::mutex захватывается и освобождается два раза: сначала в shared_mutex::lock_shared, затем в shared_mutex::unlock_shared. Для той короткой операции над разделяемым ресурсом, которая используется в тесте, это оказывается фатально. Для проверки своей гипотезы я сделал там еще простенькие имитаторы dummy_shared_mutex и dummy_shared_lock. В целом, они работают быстрее, чем shared_mutex, но все равно проигрывают std::mutex-у.

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

#include <iostream>
#include <thread>
#include <mutex>
#include <map>
#include <vector>
#include <chrono>
#include <string>

#include <ace/RW_Thread_Mutex.h>
#include <ace/Guard_T.h>

#include <shared_mutex>

class duration_meter
{
public :
   duration_meter( std::string name )
      :  name_( std::move( name ) )
      ,  start_( std::chrono::high_resolution_clock::now() )
      {}

   ~duration_meter()
      {
         auto finish = std::chrono::high_resolution_clock::now();

         std::cout << name_ << ": "
            << std::chrono::duration_cast< std::chrono::microseconds >(
                  finish - start_ ).count() << "us"
            << std::endl;
      }

private :
   const std::string name_;
   const std::chrono::high_resolution_clock::time_point start_;
};

templateclass M, class L >
class common_resource
   {
   public :
      common_resource()
         {
            map_[ "one" ] = this;
            map_[ "two" ] = this;
            map_[ "three" ] = this;
            map_[ "four" ] = this;
         }

      void *
      find( const std::string & v ) const
         {
            L lock( mutex_ );

            auto it = map_.find( v );
            return it != map_.end() ? it->second : nullptr;
         }

   private :
      mutable M mutex_;

      std::map< std::string, void * > map_;
   };

templateclass CR >
void thread_body( const CR & resource, size_t iterations )
   {
      std::string keys[] = { "1""two""3""four""5""six" };

      size_t found = 0;

      forsize_t i = 0; i != iterations; ++i )
         {
            forauto & s : keys )
               {
                  ifnullptr != resource.find( s ) )
                     ++found;
               }
         }
   }

templateclass M, class L >
void
benchmark(
   const std::string & name,
   size_t thread_count,
   size_t iterations )
   {
      common_resource< M, L > resource;

      duration_meter duration( name );

      std::vector< std::thread > threads;

      forsize_t i = 0; i != thread_count; ++i )
         {
            threads.emplace_back( std::thread(
                  [&resource, iterations]() {
                     thread_body( resource, iterations );
                  } ) );
         }

      forauto & t : threads )
         t.join();
   }

class dummy_shared_mutex
   {
   public :
      dummy_shared_mutex()
         {}

      inline void
      lock_shared()
         {
            m_.lock();
            ++readers_;
            m_.unlock();
         }

      inline void
      unlock_shared()
         {
            --readers_;
         }

   private :
      std::mutex m_;
      size_t readers_;
   };

class dummy_shared_lock
   {
   public :
      inline dummy_shared_lock( dummy_shared_mutex & m )
         :  m_( m )
         {
            m.lock_shared();
         }
      inline ~dummy_shared_lock()
         {
            m_.unlock_shared();
         }
   private :
      dummy_shared_mutex & m_;
   };

int
main( int argc, char ** argv )
   {
      if3 != argc )
         {
            std::cout << "Usage:\n\n"
               "ace_vs_std <thread_count> <iterations>"
               << std::endl;
            return 1;
         }

      const size_t thread_count = std::atoi( argv[1] );
      const size_t iterations = std::atoi( argv[2] );

      benchmark< std::mutex, std::lock_guard< std::mutex > >(
            "std::mutex and std::lock_guard",
            thread_count,
            iterations );

      benchmark< ting::shared_mutex, ting::shared_lock< ting::shared_mutex > >(
            "shared_mutex and shared_lock",
            thread_count,
            iterations );

      benchmark< dummy_shared_mutex, dummy_shared_lock >(
            "dummy_shared_mutex and dummy_shared_lock",
            thread_count,
            iterations );

      benchmark< ACE_RW_Thread_Mutex, ACE_Read_Guard< ACE_RW_Thread_Mutex > >(
            "ACE RW_Thread_Mutex",
            thread_count,
            iterations );

      return 0;
   }

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