quelle est la performance de std::mutex
par rapport à CRITICAL_SECTION
? est-ce à la hauteur?
J'ai besoin d'un objet de synchronisation léger (ne doit pas nécessairement être un objet interprocess). Existe-t-il une classe STL proche de CRITICAL_SECTION
autre que std::mutex
?
Veuillez consulter mes mises à jour à la fin de la réponse. La situation a radicalement changé depuis Visual Studio 2015. La réponse d'origine est ci-dessous.
J'ai fait un test très simple et selon mes mesures, le std::mutex
est environ 50-70x plus lent que le CRITICAL_SECTION
.
std::mutex: 18140574us
CRITICAL_SECTION: 296874us
Edit: Après quelques tests supplémentaires, il est apparu que cela dépendait du nombre de threads (encombrement) et du nombre de cœurs de processeur. Généralement, le std::mutex
est plus lent, mais dans quelle mesure, cela dépend de son utilisation. Voici les résultats des tests mis à jour (testés sur MacBook Pro avec Core i5-4258U, Windows 10, Bootcamp):
Iterations: 1000000
Thread count: 1
std::mutex: 78132us
CRITICAL_SECTION: 31252us
Thread count: 2
std::mutex: 687538us
CRITICAL_SECTION: 140648us
Thread count: 4
std::mutex: 1031277us
CRITICAL_SECTION: 703180us
Thread count: 8
std::mutex: 86779418us
CRITICAL_SECTION: 1634123us
Thread count: 16
std::mutex: 172916124us
CRITICAL_SECTION: 3390895us
Voici le code qui a généré cette sortie. Compilé avec Visual Studio 2012, paramètres de projet par défaut, configuration de la version Win32. Veuillez noter que ce test n'est peut-être pas tout à fait correct, mais cela m'a fait réfléchir à deux fois avant de passer mon code de CRITICAL_SECTION
à std::mutex
.
#include "stdafx.h"
#include <Windows.h>
#include <mutex>
#include <thread>
#include <vector>
#include <chrono>
#include <iostream>
const int g_cRepeatCount = 1000000;
const int g_cThreadCount = 16;
double g_shmem = 8;
std::mutex g_mutex;
CRITICAL_SECTION g_critSec;
void sharedFunc( int i )
{
if ( i % 2 == 0 )
g_shmem = sqrt(g_shmem);
else
g_shmem *= g_shmem;
}
void threadFuncCritSec() {
for ( int i = 0; i < g_cRepeatCount; ++i ) {
EnterCriticalSection( &g_critSec );
sharedFunc(i);
LeaveCriticalSection( &g_critSec );
}
}
void threadFuncMutex() {
for ( int i = 0; i < g_cRepeatCount; ++i ) {
g_mutex.lock();
sharedFunc(i);
g_mutex.unlock();
}
}
void testRound(int threadCount)
{
std::vector<std::thread> threads;
auto startMutex = std::chrono::high_resolution_clock::now();
for (int i = 0; i<threadCount; ++i)
threads.Push_back(std::thread( threadFuncMutex ));
for ( std::thread& thd : threads )
thd.join();
auto endMutex = std::chrono::high_resolution_clock::now();
std::cout << "std::mutex: ";
std::cout << std::chrono::duration_cast<std::chrono::microseconds>(endMutex - startMutex).count();
std::cout << "us \n\r";
threads.clear();
auto startCritSec = std::chrono::high_resolution_clock::now();
for (int i = 0; i<threadCount; ++i)
threads.Push_back(std::thread( threadFuncCritSec ));
for ( std::thread& thd : threads )
thd.join();
auto endCritSec = std::chrono::high_resolution_clock::now();
std::cout << "CRITICAL_SECTION: ";
std::cout << std::chrono::duration_cast<std::chrono::microseconds>(endCritSec - startCritSec).count();
std::cout << "us \n\r";
}
int _tmain(int argc, _TCHAR* argv[]) {
InitializeCriticalSection( &g_critSec );
std::cout << "Iterations: " << g_cRepeatCount << "\n\r";
for (int i = 1; i <= g_cThreadCount; i = i*2) {
std::cout << "Thread count: " << i << "\n\r";
testRound(i);
Sleep(1000);
}
DeleteCriticalSection( &g_critSec );
// Added 10/27/2017 to try to prevent the compiler to completely
// optimize out the code around g_shmem if it wouldn't be used anywhere.
std::cout << "Shared variable value: " << g_shmem << std::endl;
getchar();
return 0;
}
Mise à jour 27/10/2017 (1): Certaines réponses suggèrent qu'il ne s'agit pas d'un test réaliste ou d'un scénario "dans le monde réel". C’est vrai, ce test essaie de mesurer le overhead du std::mutex
, il n’essaie pas de prouver que la différence est négligeable pour 99% des applications.
Update 10/27/2017 (2): On dirait que la situation a changé en faveur de std::mutex
depuis Visual Studio 2015 (VC140). J'ai utilisé VS2017 IDE, exactement le même code que ci-dessus, la configuration de l'édition x64, les optimisations désactivées et j'ai simplement basculé le "Platform Toolset" pour chaque test. Les résultats sont très surprenants et je suis vraiment curieux de savoir ce qui est resté dans VC140.
Le test de waldez n'est pas réaliste, il simule fondamentalement 100% de conflits. En général, c'est exactement ce que vous ne voulez pas dans un code multithread. Vous trouverez ci-dessous un test modifié qui effectue des calculs partagés. Les résultats obtenus avec ce code sont différents:
Tasks: 160000
Thread count: 1
std::mutex: 12096ms
CRITICAL_SECTION: 12060ms
Thread count: 2
std::mutex: 5206ms
CRITICAL_SECTION: 5110ms
Thread count: 4
std::mutex: 2643ms
CRITICAL_SECTION: 2625ms
Thread count: 8
std::mutex: 1632ms
CRITICAL_SECTION: 1702ms
Thread count: 12
std::mutex: 1227ms
CRITICAL_SECTION: 1244ms
Vous pouvez voir ici que pour moi (avec VS2013) les chiffres sont très proches entre std :: mutex et CRITICAL_SECTION. Notez que ce code effectue un nombre fixe de tâches (160 000), raison pour laquelle les performances s'améliorent généralement avec plus de threads. J'ai 12 noyaux ici, c'est pourquoi je me suis arrêté à 12.
Je ne dis pas que c'est vrai ou faux par rapport à l'autre test, mais cela montre que les problèmes de synchronisation sont généralement spécifiques à un domaine.
#include "stdafx.h"
#include <Windows.h>
#include <mutex>
#include <thread>
#include <vector>
#include <chrono>
#include <iostream>
const int tastCount = 160000;
int numThreads;
const int MAX_THREADS = 16;
double g_shmem = 8;
std::mutex g_mutex;
CRITICAL_SECTION g_critSec;
void sharedFunc(int i, double &data)
{
for (int j = 0; j < 100; j++)
{
if (j % 2 == 0)
data = sqrt(data);
else
data *= data;
}
}
void threadFuncCritSec() {
double lMem = 8;
int iterations = tastCount / numThreads;
for (int i = 0; i < iterations; ++i) {
for (int j = 0; j < 100; j++)
sharedFunc(j, lMem);
EnterCriticalSection(&g_critSec);
sharedFunc(i, g_shmem);
LeaveCriticalSection(&g_critSec);
}
printf("results: %f\n", lMem);
}
void threadFuncMutex() {
double lMem = 8;
int iterations = tastCount / numThreads;
for (int i = 0; i < iterations; ++i) {
for (int j = 0; j < 100; j++)
sharedFunc(j, lMem);
g_mutex.lock();
sharedFunc(i, g_shmem);
g_mutex.unlock();
}
printf("results: %f\n", lMem);
}
void testRound()
{
std::vector<std::thread> threads;
auto startMutex = std::chrono::high_resolution_clock::now();
for (int i = 0; i < numThreads; ++i)
threads.Push_back(std::thread(threadFuncMutex));
for (std::thread& thd : threads)
thd.join();
auto endMutex = std::chrono::high_resolution_clock::now();
std::cout << "std::mutex: ";
std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(endMutex - startMutex).count();
std::cout << "ms \n\r";
threads.clear();
auto startCritSec = std::chrono::high_resolution_clock::now();
for (int i = 0; i < numThreads; ++i)
threads.Push_back(std::thread(threadFuncCritSec));
for (std::thread& thd : threads)
thd.join();
auto endCritSec = std::chrono::high_resolution_clock::now();
std::cout << "CRITICAL_SECTION: ";
std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(endCritSec - startCritSec).count();
std::cout << "ms \n\r";
}
int _tmain(int argc, _TCHAR* argv[]) {
InitializeCriticalSection(&g_critSec);
std::cout << "Tasks: " << tastCount << "\n\r";
for (numThreads = 1; numThreads <= MAX_THREADS; numThreads = numThreads * 2) {
if (numThreads == 16)
numThreads = 12;
Sleep(100);
std::cout << "Thread count: " << numThreads << "\n\r";
testRound();
}
DeleteCriticalSection(&g_critSec);
return 0;
}
J'utilise Visual Studio 2013.
Mes résultats en utilisation mono-thread ressemblent aux résultats waldez:
1 million d'appels de verrouillage/déverrouillage:
CRITICAL_SECTION: 19 ms
std::mutex: 48 ms
std::recursive_mutex: 48 ms
La raison pour laquelle Microsoft a changé d'implémentation est la compatibilité C++ 11 . C++ 11 a 4 types de mutex dans l'espace de noms std:
Microsoft std :: mutex et tous les autres mutex sont les enveloppes de la section critique:
struct _Mtx_internal_imp_t
{ /* Win32 mutex */
int type; // here MS keeps particular mutex type
Concurrency::critical_section cs;
long thread_id;
int count;
};
Pour moi, std :: recursive_mutex devrait correspondre parfaitement à la section critique. Microsoft devrait donc optimiser son implémentation pour prendre moins de temps processeur et de mémoire.
Je cherchais ici des repères pthread contre des sections critiques, mais comme mon résultat était différent de la réponse de waldez en ce qui concerne le sujet, j'ai pensé qu'il serait intéressant de le partager.
Le code est celui utilisé par @waldez, modifié pour ajouter des pthreads à la comparaison, compilé avec GCC et aucune optimisation. Mon processeur est AMD A8-3530MX.
Windows 7 Édition familiale:
>a.exe
Iterations: 1000000
Thread count: 1
std::mutex: 46800us
CRITICAL_SECTION: 31200us
pthreads: 31200us
Thread count: 2
std::mutex: 171600us
CRITICAL_SECTION: 218400us
pthreads: 124800us
Thread count: 4
std::mutex: 327600us
CRITICAL_SECTION: 374400us
pthreads: 249600us
Thread count: 8
std::mutex: 967201us
CRITICAL_SECTION: 748801us
pthreads: 717601us
Thread count: 16
std::mutex: 2745604us
CRITICAL_SECTION: 1497602us
pthreads: 1903203us
Comme vous pouvez le constater, la différence varie beaucoup au sein d'une erreur statistique - parfois, std :: mutex est plus rapide, parfois non. Ce qui est important, je n’observe pas une aussi grande différence que la réponse originale.
Je pense, peut-être que la raison en est que, lorsque la réponse a été publiée, le compilateur MSVC n’était pas compatible avec les normes les plus récentes et que la réponse originale utilisait la version de 2012.
Aussi, par curiosité, même binaire sous Wine sur Archlinux:
$ wine a.exe
fixme:winediag:start_process Wine Staging 2.19 is a testing version containing experimental patches.
fixme:winediag:start_process Please mention your exact version when filing bug reports on winehq.org.
Iterations: 1000000
Thread count: 1
std::mutex: 53810us
CRITICAL_SECTION: 95165us
pthreads: 62316us
Thread count: 2
std::mutex: 604418us
CRITICAL_SECTION: 1192601us
pthreads: 688960us
Thread count: 4
std::mutex: 779817us
CRITICAL_SECTION: 2476287us
pthreads: 818022us
Thread count: 8
std::mutex: 1806607us
CRITICAL_SECTION: 7246986us
pthreads: 809566us
Thread count: 16
std::mutex: 2987472us
CRITICAL_SECTION: 14740350us
pthreads: 1453991us
Le code de waldez avec mes modifications:
#include <math.h>
#include <windows.h>
#include <mutex>
#include <thread>
#include <vector>
#include <chrono>
#include <iostream>
#include <pthread.h>
const int g_cRepeatCount = 1000000;
const int g_cThreadCount = 16;
double g_shmem = 8;
std::mutex g_mutex;
CRITICAL_SECTION g_critSec;
pthread_mutex_t pt_mutex;
void sharedFunc( int i )
{
if ( i % 2 == 0 )
g_shmem = sqrt(g_shmem);
else
g_shmem *= g_shmem;
}
void threadFuncCritSec() {
for ( int i = 0; i < g_cRepeatCount; ++i ) {
EnterCriticalSection( &g_critSec );
sharedFunc(i);
LeaveCriticalSection( &g_critSec );
}
}
void threadFuncMutex() {
for ( int i = 0; i < g_cRepeatCount; ++i ) {
g_mutex.lock();
sharedFunc(i);
g_mutex.unlock();
}
}
void threadFuncPTMutex() {
for ( int i = 0; i < g_cRepeatCount; ++i ) {
pthread_mutex_lock(&pt_mutex);
sharedFunc(i);
pthread_mutex_unlock(&pt_mutex);
}
}
void testRound(int threadCount)
{
std::vector<std::thread> threads;
auto startMutex = std::chrono::high_resolution_clock::now();
for (int i = 0; i<threadCount; ++i)
threads.Push_back(std::thread( threadFuncMutex ));
for ( std::thread& thd : threads )
thd.join();
auto endMutex = std::chrono::high_resolution_clock::now();
std::cout << "std::mutex: ";
std::cout << std::chrono::duration_cast<std::chrono::microseconds>(endMutex - startMutex).count();
std::cout << "us \n";
g_shmem = 0;
threads.clear();
auto startCritSec = std::chrono::high_resolution_clock::now();
for (int i = 0; i<threadCount; ++i)
threads.Push_back(std::thread( threadFuncCritSec ));
for ( std::thread& thd : threads )
thd.join();
auto endCritSec = std::chrono::high_resolution_clock::now();
std::cout << "CRITICAL_SECTION: ";
std::cout << std::chrono::duration_cast<std::chrono::microseconds>(endCritSec - startCritSec).count();
std::cout << "us \n";
g_shmem = 0;
threads.clear();
auto startPThread = std::chrono::high_resolution_clock::now();
for (int i = 0; i<threadCount; ++i)
threads.Push_back(std::thread( threadFuncPTMutex ));
for ( std::thread& thd : threads )
thd.join();
auto endPThread = std::chrono::high_resolution_clock::now();
std::cout << "pthreads: ";
std::cout << std::chrono::duration_cast<std::chrono::microseconds>(endPThread - startPThread).count();
std::cout << "us \n";
g_shmem = 0;
}
int main() {
InitializeCriticalSection( &g_critSec );
pthread_mutex_init(&pt_mutex, 0);
std::cout << "Iterations: " << g_cRepeatCount << "\n";
for (int i = 1; i <= g_cThreadCount; i = i*2) {
std::cout << "Thread count: " << i << "\n";
testRound(i);
Sleep(1000);
}
getchar();
DeleteCriticalSection( &g_critSec );
pthread_mutex_destroy(&pt_mutex);
return 0;
}
Même programme test de Waldez modifié pour fonctionner avec pthreads et boost :: mutex.
Sur win10 pro (avec les processeurs Intel i7-7820X 16 cœurs), std :: mutex sur VS2015 update3 donne de meilleurs résultats (et même meilleur de boost :: mutex) à partir de CRITICAL_SECTION :
Iterations: 1000000
Thread count: 1
std::mutex: 23403us
boost::mutex: 12574us
CRITICAL_SECTION: 19454us
Thread count: 2
std::mutex: 55031us
boost::mutex: 45263us
CRITICAL_SECTION: 187597us
Thread count: 4
std::mutex: 113964us
boost::mutex: 83699us
CRITICAL_SECTION: 605765us
Thread count: 8
std::mutex: 266091us
boost::mutex: 155265us
CRITICAL_SECTION: 1908491us
Thread count: 16
std::mutex: 633032us
boost::mutex: 300076us
CRITICAL_SECTION: 4015176us
Les résultats pour les pthreads sont here .
#ifdef _WIN32
#include <Windows.h>
#endif
#include <mutex>
#include <boost/thread/mutex.hpp>
#include <thread>
#include <vector>
#include <chrono>
#include <iostream>
const int g_cRepeatCount = 1000000;
const int g_cThreadCount = 16;
double g_shmem = 8;
std::recursive_mutex g_mutex;
boost::mutex g_boostMutex;
void sharedFunc(int i)
{
if (i % 2 == 0)
g_shmem = sqrt(g_shmem);
else
g_shmem *= g_shmem;
}
#ifdef _WIN32
CRITICAL_SECTION g_critSec;
void threadFuncCritSec()
{
for (int i = 0; i < g_cRepeatCount; ++i)
{
EnterCriticalSection(&g_critSec);
sharedFunc(i);
LeaveCriticalSection(&g_critSec);
}
}
#else
pthread_mutex_t pt_mutex;
void threadFuncPtMutex()
{
for (int i = 0; i < g_cRepeatCount; ++i) {
pthread_mutex_lock(&pt_mutex);
sharedFunc(i);
pthread_mutex_unlock(&pt_mutex);
}
}
#endif
void threadFuncMutex()
{
for (int i = 0; i < g_cRepeatCount; ++i)
{
g_mutex.lock();
sharedFunc(i);
g_mutex.unlock();
}
}
void threadFuncBoostMutex()
{
for (int i = 0; i < g_cRepeatCount; ++i)
{
g_boostMutex.lock();
sharedFunc(i);
g_boostMutex.unlock();
}
}
void testRound(int threadCount)
{
std::vector<std::thread> threads;
std::cout << "\nThread count: " << threadCount << "\n\r";
auto startMutex = std::chrono::high_resolution_clock::now();
for (int i = 0; i < threadCount; ++i)
threads.Push_back(std::thread(threadFuncMutex));
for (std::thread& thd : threads)
thd.join();
threads.clear();
auto endMutex = std::chrono::high_resolution_clock::now();
std::cout << "std::mutex: ";
std::cout << std::chrono::duration_cast<std::chrono::microseconds>(endMutex - startMutex).count();
std::cout << "us \n\r";
auto startBoostMutex = std::chrono::high_resolution_clock::now();
for (int i = 0; i < threadCount; ++i)
threads.Push_back(std::thread(threadFuncBoostMutex));
for (std::thread& thd : threads)
thd.join();
threads.clear();
auto endBoostMutex = std::chrono::high_resolution_clock::now();
std::cout << "boost::mutex: ";
std::cout << std::chrono::duration_cast<std::chrono::microseconds>(endBoostMutex - startBoostMutex).count();
std::cout << "us \n\r";
#ifdef _WIN32
auto startCritSec = std::chrono::high_resolution_clock::now();
for (int i = 0; i < threadCount; ++i)
threads.Push_back(std::thread(threadFuncCritSec));
for (std::thread& thd : threads)
thd.join();
threads.clear();
auto endCritSec = std::chrono::high_resolution_clock::now();
std::cout << "CRITICAL_SECTION: ";
std::cout << std::chrono::duration_cast<std::chrono::microseconds>(endCritSec - startCritSec).count();
std::cout << "us \n\r";
#else
auto startPThread = std::chrono::high_resolution_clock::now();
for (int i = 0; i < threadCount; ++i)
threads.Push_back(std::thread(threadFuncPtMutex));
for (std::thread& thd : threads)
thd.join();
threads.clear();
auto endPThread = std::chrono::high_resolution_clock::now();
std::cout << "pthreads: ";
std::cout << std::chrono::duration_cast<std::chrono::microseconds>(endPThread - startPThread).count();
std::cout << "us \n";
#endif
}
int main()
{
#ifdef _WIN32
InitializeCriticalSection(&g_critSec);
#else
pthread_mutex_init(&pt_mutex, 0);
#endif
std::cout << "Iterations: " << g_cRepeatCount << "\n\r";
for (int i = 1; i <= g_cThreadCount; i = i * 2)
{
testRound(i);
std::this_thread::sleep_for(std::chrono::seconds(1));
}
#ifdef _WIN32
DeleteCriticalSection(&g_critSec);
#else
pthread_mutex_destroy(&pt_mutex);
#endif
if (Rand() % 10000 == 1)
{
// Added 10/27/2017 to try to prevent the compiler to completely
// optimize out the code around g_shmem if it wouldn't be used anywhere.
std::cout << "Shared variable value: " << g_shmem << std::endl;
}
return 0;
}