44 votes

Performances de std::mutex par rapport à win32 CRITICAL_SECTION

Comment la performance de std::mutex par rapport à CRITICAL_SECTION ? c'est à la hauteur ?

J'ai besoin d'un objet de synchronisation léger (qui n'a pas besoin d'être un objet interprocessus). Existe-t-il une classe STL qui se rapproche de l'objet de synchronisation ? CRITICAL_SECTION autre que std::mutex ?

48voto

waldez Points 66

Veuillez consulter mes mises à jour à la fin de la réponse, la situation a radicalement changé depuis Visual Studio 2015. La réponse originale est ci-dessous.

J'ai fait un test très simple et d'après mes mesures, la std::mutex est environ 50-70x plus lent que CRITICAL_SECTION .

std::mutex:       18140574us
CRITICAL_SECTION: 296874us

Edit : Après quelques tests supplémentaires, il s'est avéré que cela dépend du nombre de threads (congestion) et du nombre de cœurs du CPU. En général, le std::mutex est plus lent, mais de combien, cela dépend de l'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 produit ce résultat. 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 parfaitement correct, mais il m'a fait réfléchir à deux fois avant de changer mon code pour passer de l'utilisation 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 du 27/10/2017 (1) : Certaines réponses suggèrent que ce test n'est pas réaliste ou ne représente pas un scénario du "monde réel". C'est vrai, ce test tente de mesurer la Transparent de la std::mutex il ne s'agit pas de prouver que la différence est négligeable pour 99 % des applications.

Mise à jour du 27/10/2017 (2) : Il semble que la situation ait changé en faveur de std::mutex depuis Visual Studio 2015 (VC140). J'ai utilisé l'IDE VS2017, exactement le même code que ci-dessus, configuration de la version x64, optimisations désactivées et j'ai simplement changé le "Platform Toolset" pour chaque test. Les résultats sont très surprenants et je suis vraiment curieux de savoir ce qui a changé dans VC140.

Performance with 8 threads, by platform

Mise à jour 25/02/2020 (3) : Reran le test avec Visual Studio 2019 (Toolset v142), et la situation est toujours la même : std::mutex est deux à trois fois plus rapide que CRITICAL_SECTION .

25voto

Superfly Jon Points 101

Le test effectué par Waldez ici n'est pas réaliste, il simule essentiellement une contention à 100%. En général, c'est exactement ce que vous ne voulez pas dans un code multithread. Voici un test modifié qui effectue quelques calculs partagés. Les résultats que j'obtiens 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), c'est pourquoi les performances s'améliorent généralement avec plus de threads. Je dispose de 12 cœurs ici, c'est pourquoi je me suis arrêté à 12.

Je ne dis pas que c'est bien ou mal par rapport à l'autre test, mais cela souligne que les problèmes de timing 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;
}

5voto

Bo Persson Points 42821

Je m'attendrais à un std::mutex à mettre en œuvre comme une CRITCAL_SECTION sous Windows.

Donc aucune différence de performance.

2voto

Sergey Points 185

J'utilise Visual Studio 2013.

Mes résultats en utilisation monotone sont similaires à ceux de 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 modifié l'implémentation est la compatibilité avec 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 ma part, std::recursive_mutex devrait correspondre complètement à la section critique. Microsoft devrait donc optimiser son implémentation pour qu'elle prenne moins de CPU et de mémoire.

2voto

Yagamy Light Points 307

Je cherchais ici les benchmarks pthread vs critical section, cependant, comme mon résultat s'est avéré être différent de la réponse de waldez concernant le sujet, j'ai pensé qu'il serait intéressant de le partager.

Le code est celui utilisé par @waldez, modifié pour ajouter pthreads à la comparaison, compilé avec GCC et sans optimisations. Mon processeur est un AMD A8-3530MX.

Windows 7 Home Edition :

>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 voir, la différence varie bien dans les limites de l'erreur statistique - parfois std::mutex est plus rapide, parfois non. Ce qui est important, c'est que je n'observe pas de différence aussi importante que dans la réponse originale.

Je pense, peut-être que la raison est que lorsque la réponse a été postée, le compilateur MSVC n'était pas bon avec les nouveaux standards, et notez que la réponse originale a utilisé la version de l'année 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;
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X