IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Threads & Processus C++ Discussion :

Problème de lenteur du multi-threading


Sujet :

Threads & Processus C++

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre chevronné
    Avatar de ABD-Z
    Homme Profil pro
    Ingé. webapps embarquées – Admin/mainteneur serveur/BDD – Formateur WordPress – Desiger : logo/site
    Inscrit en
    Septembre 2016
    Messages
    302
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 28
    Localisation : France, Val de Marne (Île de France)

    Informations professionnelles :
    Activité : Ingé. webapps embarquées – Admin/mainteneur serveur/BDD – Formateur WordPress – Desiger : logo/site

    Informations forums :
    Inscription : Septembre 2016
    Messages : 302
    Billets dans le blog
    3
    Par défaut Problème de lenteur du multi-threading
    Bonjour à vous tous!
    Je me retourne vers vous, les pros, pour avoir vos opinions sur le multi-threading, et plus précisément en C++.

    Je souhaite en fait améliorer les performances de mon séquenceur de musique temps réel (voir branche performance) en apportant justement du multi-treading.

    Pour faire simple, le séquenceur peut être composé de N canaux.
    Chaque canal, à un instant T, calcul son propre échantillon audio. Les résultats de chaque canal vont être à la fin additionnés pour former l’échantillon audio de la musique à l’instant T.
    On additionne N échantillons pour former un échantillon audio effectif qui sera sauvegarder dans le tampon audio.

    Cette opération de calcul des échantillons au niveau des canaux se fait séquentiellement : c’est-à-dire qu’on commence par calculer l’échantillon du canal N-1, puis celui du canal N-2 jusqu’au canal 0.
    (oui t’as capté, commencer la boucle par N-1 c’est plus rapide!)

    Je me suis dit : «Tiens, ça serait pas mal si l’on parallélisait tout ça !».
    Surtout que les canaux n’ont pas d’interdépendances ; chaque canal a ses propres données et donc pas de prise de tête avec la concurrence, de ressources bien entendu.

    Du coup, avant de me lancer dans la modification de mon projet, j’ai fait un croquis pour simuler l’exécution en séquentielle et en «parallèle» (oui je me des guillemets parce qu’on sait tous que les threads c’est pas forcément parallèle en réalité) et ainsi comparer leur temps d’exécution.

    Dans le fichier de simulation (disponible, j’ai déclaré trois constantes en #define.
    CHANSc’est le nombre de canaux que l’on va créer.
    COUNTle nombre de fois que l’on va répéter le «calcul d’échantillonnage»
    LOOPSest utilisé dans les boucles for pour faire écouler du temps et ainsi simuler le temps de «calcul d’échantillonnage» au niveau du canal.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    #define CHANS 2
    #define COUNT 10
    #define LOOPS 666
    Premièrement, on a la classe Seq_Chan qui sera utilisé pour simuler le calcul séquentiel.
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Seq_Chan {
    private:
    	unsigned char id = 0;
    	static unsigned char chancount;
    public:
    	Seq_Chan() {
            this->id = Seq_Chan::chancount++;
        }
     
        void play()
        {
        	//std::cout << "Seq Chan " << +this->id <<" play  " << std::endl;
        	for(int i = 0; i<LOOPS; ++i);
        	//std::cout << "Seq Chan " << +this->id <<" finish play  " << std::endl;
        }
    };
    unsigned char Seq_Chan::chancount = 0;
    Dans le main rien de plus simple :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    Seq_Chan seq_chan[CHANS];
    	for(unsigned int j = 0; j < COUNT; ++j){
    		//std::cout << "*******************************"<<j<<"*******************************"<<std::endl;
    		for(char i=(CHANS-1); i >=0; --i){
    			seq_chan[i].play();
    		}
     
    	}
    Deuxièmement, concernant la classe qui gère les threads, Chan,elle stocke le std::thread (processor) et une structure Mutexcontenant deux std::mutex (maint2thread, thread2main) verrouillées à partir du constructeur. L’important ici c’est de pouvoir synchroniser avec le thread principal C’est pour ça que deux mutex sont nécessaires : une pour donner le départ, et une pour avertir de la fin.
    La classe dispose donc d’une méthode pour accéder à la structure contenant les mutex.
    Dans la méthode play, on verrouille main2threadpour bien synchroniser.
    Il faudra déverrouiller main2threaddans le main afin d’exécuter la suite de la méthode play(première boucle for dans le main).
    La deuxième boucle for dans le main verrouille thread2mainpour synchroniser le main avec les threads. Dans la méthode play, avant de reboucler, thread2mainest déverrouillé.
    Nom : threadingsynch.png
Affichages : 343
Taille : 21,3 Ko
    Schéma illustrant la synchronisation
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    struct Mutex{
        std::mutex maint2thread, thread2main;
    };
     
    class Chan {
    private:
    	std::thread processor;
    	Mutex mut;
    	unsigned char id = 0;
    	static unsigned char chancount;
    	bool stopped = false;
    public:
    	Chan() {
    		std::thread p(&Chan::play, this);
            this->processor = std::move(p);
            mut.maint2thread.lock();
            mut.thread2main.lock();
            this->id = Chan::chancount++;
        }
     
        void play()
        {
        	while(true){
        		this->mut.maint2thread.lock();
        		if(this->stopped){
        			break;
        		}
        	    //std::cout << "Chan " <<  +this->id <<" play thread " << std::endl;
        	    for(int i = 0; i<LOOPS; ++i);
        	    //std::cout << "Chan " <<  +this->id <<" finish play thread " << std::endl;
     
        		this->mut.thread2main.unlock();
        	}
     
        }
     
        void wait(){
        	std::cout << "wait processor " <<  +this->id << " for finish" << std::endl;
        	this->processor.join();
        }
     
        void stop(){
        	std::cout << "stop processor " <<  +this->id << std::endl;
        	this->stopped = true;
        	this->mut.maint2thread.unlock();
        }
     
        Mutex* getMut(){
        	return &this->mut;
        }
     
    };
     
    int main(){
    	Chan chan[CHANS];
    	for(unsigned int j = 0; j < COUNT; ++j){
    		//std::cout << "*******************************"<<j<<"*******************************"<<std::endl;
    		for(short i=(CHANS-1); i >=0; --i){
    			chan[i].getMut()->maint2thread.unlock();
    		}
     
    		for(short i=(CHANS-1); i >=0; --i){
    			chan[i].getMut()->thread2main.lock();	
    		}
    		//std::cout<<"----add samples-----"<< std::endl;
    	}
     
    	for(short i=(CHANS-1); i >=0; --i){
    		chan[i].stop();	
    	}
     
    	for(short i=(CHANS-1); i >=0; --i){
    		chan[i].wait();	
    	}
    }
    Eh bien, en chronométrant le temps écoulé pour le calcul séquentiel et pour le calcul «parallèle», je me suis rendu compte que l’approche multi-threadé est intéressante lorsque la fonction play est vraiment coûteuse en temps. Or, en réalité, mon séquenceur calcule l’échantillon d’un canal en 900 ns jusqu’à à peu près 20000 ns (à cause des traitements d’effets sûrement). Et j’ai testé en mettant LOOPS à 66 (900 ns sur ma machine) et à 6666 (18000 ns) voire même plus et les résultats me montrent que l’approche threadé n’est pas du tout rentable pour mon cas! Du moins, vu comment je l’ai implémenté.
    Pourtant en théorie, ça doit être beaucoup plus performant qu’en séquentielle.
    Je met le doigt sur les boucles for dans le main gérant les mutex et sur la performance des threads (changement de pile, contexte etc... c’est pas du vrai parallélisme).

    Ma question : déjà, est-ce que j’ai bien implémenté l’approche threadé ? Sachant que la synchronisation est obligatoire pour mon cas ou sinon j’aurais des threads qui iront plus vite que la musqiue !

    Deuxième question : comment vraiment paralléliser ? Et surtout efficacement. Le processeur de ma machine est quadricœur, ça devrait donc le faire !
    Fichiers attachés Fichiers attachés

  2. #2
    Expert confirmé
    Homme Profil pro
    Ingénieur développement matériel électronique
    Inscrit en
    Décembre 2015
    Messages
    1 599
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 62
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Ingénieur développement matériel électronique
    Secteur : High Tech - Électronique et micro-électronique

    Informations forums :
    Inscription : Décembre 2015
    Messages : 1 599
    Par défaut
    Citation Envoyé par ABD-Z Voir le message
    Ma question : déjà, est-ce que j’ai bien implémenté l’approche threadé ? Sachant que la synchronisation est obligatoire pour mon cas ou sinon j’aurais des threads qui iront plus vite que la musqiue !
    Non.
    Tu n'as pas bien compris à quoi servent les mutex. Un mutex ça permet l'exclusion mutuelle et rien d'autre. Pour la synchronisation tu as tous les autres objets: condition_variable, semaphore, barrier, future, ...
    Tes mutex se mettent en condition d'erreur (rendus alors qu'ils n'ont pas été pris) et ne gèrent rien.
    Citation Envoyé par ABD-Z Voir le message
    Deuxième question : comment vraiment paralléliser ? Et surtout efficacement. Le processeur de ma machine est quadricœur, ça devrait donc le faire !
    En utilisant des objets de synchro. Un exemple avec une condition_variable, mais on peut faire mieux.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    class Chan {
    private:
    	std::thread processor;
    	mutable std::condition_variable the_cv;
    	mutable std::mutex the_mutex;
    	enum class State { Wait, Begin, End, Terminate } state{State::Wait};
    	unsigned char id = 0;
    	static unsigned char chancount;
    public:
    	Chan() {
    		id = chancount++;
    		processor = std::thread{&Chan::play, this};
    	}
    	~Chan() {
    		stop();
    		wait();
    	}
     
    	void play() {
    		while (true) {
    			// wait for start
    			{
    				std::unique_lock<std::mutex> lk(the_mutex);
    				  the_cv.wait( lk, [this]{return state == State::Begin || state == State::Terminate;} );
    			}
    			if ( state == State::Terminate ) {
    				break;
    			}
    			//std::cout << "Chan " <<  +this->id <<" play thread " << std::endl;
    			for (volatile int i = 0; i < LOOPS; ++i);
    			//std::cout << "Chan " <<  +this->id <<" finish play thread " << std::endl;
     
    			// signals the end
    			{
    				std::unique_lock<std::mutex> lk(the_mutex);
    				  if ( state != State::Terminate )
    					state = State::End;
    				  the_cv.notify_all();
    			}
    		}
    	}
     
    	void wait() {
    		std::cout << "wait processor " << +this->id << " for finish" << std::endl;
    		if ( processor.joinable() )
    			this->processor.join();
    	}
    	void sync_begin() {
    		std::unique_lock<std::mutex> lk(the_mutex);
    		  state = State::Begin;
    		  the_cv.notify_all();
    	}
    	void sync_end() {
    		std::unique_lock<std::mutex> lk(the_mutex);
    		  the_cv.wait( lk, [this]{return state==State::End;} );
    	}
    	void stop() {
    		std::cout << "stop processor " << +this->id << std::endl;
    		std::unique_lock<std::mutex> lk(the_mutex);
    		  state = State::Terminate;
    		  the_cv.notify_all();
    	}
    };
     
    int main() {
    	Chan chan[CHANS];
    	auto debut = std::chrono::steady_clock::now();
    	for (unsigned int j = 0; j < COUNT; ++j) {
    		std::cout << "*******************************"<<j<<"*******************************"<<std::endl;
    		for (short i = (CHANS - 1); i >= 0; --i) {
    			chan[i].sync_begin();
    		}
    		// ICI TOUS LES THEADS FONT LEUR BOULOT EN PARALELLE
    		for (short i = (CHANS - 1); i >= 0; --i) {
    			chan[i].sync_end();
    		}
    		// ICI TOUS LES THEADS ONT FINI LEUR BOULOT
    		//std::cout<<"----add samples-----"<< std::endl;
    	}
    	std::cout << "duration : " << std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now()-debut).count() << "µs\n";
    }

  3. #3
    Membre chevronné
    Avatar de ABD-Z
    Homme Profil pro
    Ingé. webapps embarquées – Admin/mainteneur serveur/BDD – Formateur WordPress – Desiger : logo/site
    Inscrit en
    Septembre 2016
    Messages
    302
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 28
    Localisation : France, Val de Marne (Île de France)

    Informations professionnelles :
    Activité : Ingé. webapps embarquées – Admin/mainteneur serveur/BDD – Formateur WordPress – Desiger : logo/site

    Informations forums :
    Inscription : Septembre 2016
    Messages : 302
    Billets dans le blog
    3
    Par défaut
    Citation Envoyé par dalfab Voir le message
    mais on peut faire mieux.
    J'espère très certainement car j'ai testé ton approche et figure toi que c'est bien plus lent que la mienne.

    J'ai utilisé les mutexs car il n'y avait pas de sémaphore nativement en C++ 17 (C++ 20 oui).

  4. #4
    Expert confirmé
    Homme Profil pro
    Ingénieur développement matériel électronique
    Inscrit en
    Décembre 2015
    Messages
    1 599
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 62
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Ingénieur développement matériel électronique
    Secteur : High Tech - Électronique et micro-électronique

    Informations forums :
    Inscription : Décembre 2015
    Messages : 1 599
    Par défaut
    Citation Envoyé par ABD-Z Voir le message
    J'espère très certainement car j'ai testé ton approche et figure toi que c'est bien plus lent que la mienne.

    J'ai utilisé les mutexs car il n'y avait pas de sémaphore nativement en C++ 17 (C++ 20 oui).
    En effet, tes mutex sont en état d'erreur donc ne s'arrêtent pas quand on le leur demande, forcément ça ne peut aller que plus vite!

    Et attention un mutex ne peut pas servir de synchro (c'est forcément celui qui l'a pris qui doit le rendre, toute tentative inter thread est une impossibilité) ça ressemble aux sémaphores mais ça ne fait pas du tout la même chose. Et les sémaphores n'étaient pas dans la norme car ils s'écrivent en quelques lignes en utilisant les condition_variables. Et remarque qu'avec les sémaphores on peut implémenter quelque chose qui ressemble à un mutex, (je prends, j'agis, je rends) mais c'est impossible car le mécanisme mutex doit gérer l'"inversion de priorité", le mutex est un objet très particulier.

    Si le traitement que tu fais est très court, la prise de mutex (remarque que j'en utilise 1 pour protéger la condition_variable et l'enum associé) devient visible devant le calcul lui-même mais si on doit synchroniser il faut un mécanise de synchro, c'est incontournable.

    Je ne sais pas comment tu as fait tes comparaisons. Ça n'a de sens de faire des comparaisons que si le soft est bien en mode release optimisé. Mais si on prend ta boucle for (int i = 0; i < LOOPS; ++i);, l'optimiseur voit qu'elle ne fait rien de concret et la supprime complètement elle est donc infiniment plus rapide qu'un mutex qui fait son boulot.

  5. #5
    Membre chevronné
    Avatar de ABD-Z
    Homme Profil pro
    Ingé. webapps embarquées – Admin/mainteneur serveur/BDD – Formateur WordPress – Desiger : logo/site
    Inscrit en
    Septembre 2016
    Messages
    302
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 28
    Localisation : France, Val de Marne (Île de France)

    Informations professionnelles :
    Activité : Ingé. webapps embarquées – Admin/mainteneur serveur/BDD – Formateur WordPress – Desiger : logo/site

    Informations forums :
    Inscription : Septembre 2016
    Messages : 302
    Billets dans le blog
    3
    Par défaut
    Citation Envoyé par dalfab Voir le message
    Et attention un mutex ne peut pas servir de synchro (c'est forcément celui qui l'a pris qui doit le rendre, toute tentative inter thread est une impossibilité)
    Donc si j'ai bien compris, un mutex doit être lock et unlock dans un même thread? On peut pas faire du "spaghetti" avec?


    Citation Envoyé par dalfab Voir le message
    Je ne sais pas comment tu as fait tes comparaisons. Ça n'a de sens de faire des comparaisons que si le soft est bien en mode release optimisé. Mais si on prend ta boucle for (int i = 0; i < LOOPS; ++i);, l'optimiseur voit qu'elle ne fait rien de concret et la supprime complètement elle est donc infiniment plus rapide qu'un mutex qui fait son boulot.
    J'utilise g++ de base certainement en mode debug.
    Donc, si je dois optimiser pour le release je dois faire g++ -O2 -o <nom_exe> <nom_source> (ou -O3)?
    Je mettrais des additions à faire dans la boucle pour chaque i du coup pour que l'optimiseur ne l'enlève pas.

  6. #6
    Expert confirmé
    Homme Profil pro
    Ingénieur développement matériel électronique
    Inscrit en
    Décembre 2015
    Messages
    1 599
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 62
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Ingénieur développement matériel électronique
    Secteur : High Tech - Électronique et micro-électronique

    Informations forums :
    Inscription : Décembre 2015
    Messages : 1 599
    Par défaut
    Citation Envoyé par ABD-Z Voir le message
    Donc si j'ai bien compris, un mutex doit être lock et unlock dans un même thread? On peut pas faire du "spaghetti" avec?
    Oui; La seule possibilité est : prendre le mutex, faire une action très courte à protéger, rendre le mutex. D'ailleurs on appelle jamais directement mutex.lock() pour être sur de ne oublier la libération. Exemples:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // bon exemple
    {   std::lock_gard<std::mutex>  mon_lock{mon_mutex}; // mon_lock prend le mutex
        // zone protégée
    } // mon_lock est détruit et rend le mutex
     
    // exemple à ne pas reproduire
    {   mon_mutex.lock();
         zone_protegee();
        mon_mutex.unlock();
    } // il y a une faille dans cette méthode si zone_protegee() lance une exception, le mutex est perdu!
    Citation Envoyé par ABD-Z Voir le message
    J'utilise g++ de base certainement en mode debug.
    Donc, si je dois optimiser pour le release je dois faire g++ -O2 -o <nom_exe> <nom_source> (ou -O3)?
    Oui, il faut -O2 ou -O3 est surtout pas -g (mode debug) ni -O0 (sans optimisation).

    Citation Envoyé par ABD-Z Voir le message
    Je mettrais des additions à faire dans la boucle pour chaque i du coup pour que l'optimiseur ne l'enlève pas.
    Non, une addition dans la boucle sera aussi supprimée car le code ne produit rien de "visible".
    Dans mon exemple, j'ai utilisé une variable volatile, ça devrait suffire à forcer l'optimiseur à garder du code sans effet apparent.

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. Problème de lenteur d'un thread
    Par augur67700 dans le forum Threads & Processus
    Réponses: 10
    Dernier message: 31/05/2013, 23h52
  2. Probléme serveur multi-thread
    Par hebus44 dans le forum Entrée/Sortie
    Réponses: 2
    Dernier message: 14/11/2007, 22h32
  3. Problème Seveur multi-thread
    Par Doom2Darkness dans le forum C++
    Réponses: 14
    Dernier message: 05/06/2007, 19h32
  4. problme de multi thread
    Par L4BiN dans le forum Concurrence et multi-thread
    Réponses: 22
    Dernier message: 25/04/2007, 16h47
  5. Réponses: 11
    Dernier message: 14/02/2006, 00h26

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo