Начал работать над 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_; }; template< class 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_; }; template< class CR > void thread_body( const CR & resource, size_t iterations ) { std::string keys[] = { "1", "two", "3", "four", "5", "six" }; size_t found = 0; for( size_t i = 0; i != iterations; ++i ) { for( auto & s : keys ) { if( nullptr != resource.find( s ) ) ++found; } } } template< class 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; for( size_t i = 0; i != thread_count; ++i ) { threads.emplace_back( std::thread( [&resource, iterations]() { thread_body( resource, iterations ); } ) ); } for( auto & 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 ) { if( 3 != 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; } |
Комментариев нет:
Отправить комментарий