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++

  1. #1
    Membre éclairé
    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
    262
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 26
    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 : 262
    Points : 788
    Points
    788
    Billets dans le blog
    2
    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 : 293
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 éminent
    Homme Profil pro
    Ingénieur développement matériel électronique
    Inscrit en
    Décembre 2015
    Messages
    1 565
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 60
    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 565
    Points : 7 642
    Points
    7 642
    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 éclairé
    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
    262
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 26
    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 : 262
    Points : 788
    Points
    788
    Billets dans le blog
    2
    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 éminent
    Homme Profil pro
    Ingénieur développement matériel électronique
    Inscrit en
    Décembre 2015
    Messages
    1 565
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 60
    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 565
    Points : 7 642
    Points
    7 642
    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 éclairé
    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
    262
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 26
    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 : 262
    Points : 788
    Points
    788
    Billets dans le blog
    2
    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 éminent
    Homme Profil pro
    Ingénieur développement matériel électronique
    Inscrit en
    Décembre 2015
    Messages
    1 565
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 60
    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 565
    Points : 7 642
    Points
    7 642
    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.

  7. #7
    Membre éclairé
    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
    262
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 26
    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 : 262
    Points : 788
    Points
    788
    Billets dans le blog
    2
    Par défaut
    J'ai compilé avec les options d'optimisation et rien n'y fait... Ta version est plus lente qu'en séquentielle.
    J'ai décommenté les affichages justement pour prendre plus de temps.

  8. #8
    Expert éminent
    Homme Profil pro
    Ingénieur développement matériel électronique
    Inscrit en
    Décembre 2015
    Messages
    1 565
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 60
    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 565
    Points : 7 642
    Points
    7 642
    Par défaut
    Ça dépend beaucoup du temps de traitement. Il faut peut-être vérifier dans le cas réel.
    Un basculement de contexte ça peut prendre 0,05ms sous Windows, et ça peut se produire à chaque synchronisation. Et 0,05ms ça peut paraître petit, mais en 0,05ms on peut faire plus de 100000 instructions assembleur. Si ton traitement est plus court, il y a des chances que la mise en threads soit en effet plus lente qu'un traitement linéaire.
    Pour un système avec 4 cœurs, le plus rentable serait d'utiliser au maximum 3 threads.
    J'ai fait l'essai et chez moi (Windows+compilo MSVC C++20) et ici aussi le linéaire est plus rapide avec ta boucle (LOOP = 666 et CHANS <= 4 threads). Mais avec une boucle 20 fois plus longue ou en augmentant les threads (mon CPU a 8 cœurs x 2 threads), on voit un net gain avec les threads.

  9. #9
    Membre éprouvé
    Femme Profil pro
    ..
    Inscrit en
    Décembre 2019
    Messages
    562
    Détails du profil
    Informations personnelles :
    Sexe : Femme
    Âge : 94
    Localisation : Autre

    Informations professionnelles :
    Activité : ..

    Informations forums :
    Inscription : Décembre 2019
    Messages : 562
    Points : 1 253
    Points
    1 253
    Par défaut
    Salut,

    Dans ce genre de contexte il est peut-être plus intéressant de travailler avec un pool de threads, éventuellement en mode producers-consumer(s). Si on ne veut pas s'embêter avec tout ça il y a openmp qui fait du très bon boulot quand il s'agit de paralléliser des tâches. Avec son fonctionnement en directives, le développement est bouclé en quelques minutes.

    @ABD-Z
    Je n'ai pas lu ton code mais juste remarqué les std::cout en commentaire. Donc si tu veux voir ce qui se passe dans tes threads utilise en première approche std::cerr à la place de std::cout.

  10. #10
    Membre éclairé
    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
    262
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 26
    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 : 262
    Points : 788
    Points
    788
    Billets dans le blog
    2
    Par défaut
    Citation Envoyé par kaitlyn Voir le message
    Donc si tu veux voir ce qui se passe dans tes threads utilise en première approche std::cerr à la place de std::cout.
    J'ai changé les std::cout en std::cerr et j'ai remarqué que std::cerr ralentissent grandement le programme. En fait, j'ai pas compris l'avantage à mettre des std::cerr, qu'est-ce qu'on pourrait voir de mieux, @kaitlyn ?


    Citation Envoyé par kaitlyn Voir le message
    Dans ce genre de contexte il est peut-être plus intéressant de travailler avec un pool de threads, éventuellement en mode producers-consumer(s). Si on ne veut pas s'embêter avec tout ça il y a openmp qui fait du très bon boulot quand il s'agit de paralléliser des tâches. Avec son fonctionnement en directives, le développement est bouclé en quelques minutes.
    Je viens de jeter un coup d’œil sur OpenMP, et ça a l'air d'être vachement chouette.

  11. #11
    Membre éprouvé
    Femme Profil pro
    ..
    Inscrit en
    Décembre 2019
    Messages
    562
    Détails du profil
    Informations personnelles :
    Sexe : Femme
    Âge : 94
    Localisation : Autre

    Informations professionnelles :
    Activité : ..

    Informations forums :
    Inscription : Décembre 2019
    Messages : 562
    Points : 1 253
    Points
    1 253
    Par défaut
    std::cerr permet un affichage quasi-instantané mais dans le même temps je n'avais pas fait attention à tes std::endl en bout de tes std::cout ce qui peut effectivement avoir un effet contre-productif.

  12. #12
    Expert éminent
    Homme Profil pro
    Ingénieur développement matériel électronique
    Inscrit en
    Décembre 2015
    Messages
    1 565
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 60
    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 565
    Points : 7 642
    Points
    7 642
    Par défaut
    Bonjour,

    std::cerr est nettement plus lent que std::cout. Le premier transmet instantanément chaque caractère à la console. Le second est bufferisé, il transmet les caractères par rafales en particulier au moment de std::endl ou std::flush.
    L'intérêt de std::cerr n'est pas sa vitesse mais son synchronisme, on préfère l'utiliser pour le debug et en cas de défaillance, ainsi les messages sont plus proche de ce qu'il s'est passé. Ici tu veux tracer de threads c'est donc intéressant aux dépends de sa lenteur.

  13. #13
    Membre éclairé
    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
    262
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 26
    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 : 262
    Points : 788
    Points
    788
    Billets dans le blog
    2
    Par défaut
    Je viens de tester OpenMP et c'est trop lent.... encre plus lent que mon implémentation avec les threads... Je pense qu'en terme de rapidité je peux aussi laisser tomber std::execution...
    sur un canal qui dure à peine 1ms, j'ai l'impression que les threads ne sont pas utiles...

  14. #14
    Membre éclairé
    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
    262
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 26
    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 : 262
    Points : 788
    Points
    788
    Billets dans le blog
    2
    Par défaut
    J'ai fait un truc comme ça :
    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
    #ifdef OMP
        begin2 = std::chrono::steady_clock::now();
     
        for(unsigned int j = 0; j < COUNT; ++j){
            //DISP << "*******************************"<<j<<"*******************************"<<std::endl;
            omp_set_dynamic(0);     // Explicitly disable dynamic teams
            omp_set_num_threads(CHANS);
            #pragma omp parallel for 
            for(char i=(CHANS-1); i >=0; --i){
     
                seq_chan[i].play();
            }
     
        }
     
        end2 = std::chrono::steady_clock::now();
    OMP est intéressant quand les traitements sont vraiment longs, sinon c'est tout à fait le contraire!

  15. #15
    Membre éprouvé
    Femme Profil pro
    ..
    Inscrit en
    Décembre 2019
    Messages
    562
    Détails du profil
    Informations personnelles :
    Sexe : Femme
    Âge : 94
    Localisation : Autre

    Informations professionnelles :
    Activité : ..

    Informations forums :
    Inscription : Décembre 2019
    Messages : 562
    Points : 1 253
    Points
    1 253
    Par défaut
    Affiche ton code complet, ce sera plus simple de se faire un avis.

  16. #16
    Expert éminent
    Homme Profil pro
    Ingénieur développement matériel électronique
    Inscrit en
    Décembre 2015
    Messages
    1 565
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 60
    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 565
    Points : 7 642
    Points
    7 642
    Par défaut
    Ça dépend toujours de ce qu'il y a dans tes traitements. Dans tous les cas OMP devrait avoir des temps similaire au code que je t'ai indiqué.

  17. #17
    Membre éclairé
    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
    262
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 26
    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 : 262
    Points : 788
    Points
    788
    Billets dans le blog
    2
    Par défaut
    Voici le code, il n'y a pas de changement, j'ai juste ajouté la partie OpenMP :
    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
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    #include <iostream>
    #include <thread>
    #include <chrono>
    #include <mutex>  
    #include <chrono>
    #include <vector>
    #include <omp.h>
     
     
    #define CHANS 10
    #define COUNT 10
    #define LOOPS 666
     
    #define UNIT "µs"
    #define DISP std::cout
    #define OMP
     
     
    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;
        		}
        	    //DISP << "Chan " <<  +this->id <<" play thread " << std::endl;
        	    for(int i = 0; i<LOOPS; ++i);
        	    //DISP << "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;
        }
     
    };
     
    class Seq_Chan {
    private:
    	unsigned char id = 0;
    	static unsigned char chancount;
    public:
    	Seq_Chan() {
            this->id = Seq_Chan::chancount++;
        }
     
        void play()
        {
        	//DISP << "Seq Chan " << +this->id <<" play  " << std::endl;
        	for(int i = 0; i<LOOPS; ++i);
        	//DISP << "Seq Chan " << +this->id <<" finish play  " << std::endl;
     
        }
     
    };
     
    unsigned char Chan::chancount = 0;
    unsigned char Seq_Chan::chancount = 0; 
     
     
    int main(){
    	Chan chan[CHANS];
     
    	std::chrono::steady_clock::time_point begin0 = std::chrono::steady_clock::now();
    	for(unsigned int j = 0; j < COUNT; ++j){
    		//DISP << "*******************************"<<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;
    	}
    	std::chrono::steady_clock::time_point end0 = std::chrono::steady_clock::now();
     
    	for(short i=(CHANS-1); i >=0; --i){
    		chan[i].stop();	
    	}
     
    	for(short i=(CHANS-1); i >=0; --i){
    		chan[i].wait();	
    	}
     
    	std::cout << "///////////////////////////////////////////////////////////////////////"<<std::endl;
     
    	std::chrono::steady_clock::time_point begin1 = std::chrono::steady_clock::now();
        Seq_Chan seq_chan[CHANS];
    	for(unsigned int j = 0; j < COUNT; ++j){
    		//DISP << "*******************************"<<j<<"*******************************"<<std::endl;
    		for(char i=(CHANS-1); i >=0; --i){
    			seq_chan[i].play();
    		}
     
    	}
     
    	std::chrono::steady_clock::time_point end1 = std::chrono::steady_clock::now();
     
    	std::cout << "///////////////////////////////////////////////////////////////////////"<<std::endl;
     
     
        #ifdef OMP
        std::chrono::steady_clock::time_point begin2, end2;
        begin2 = std::chrono::steady_clock::now();
     
        for(unsigned int j = 0; j < COUNT; ++j){
            //DISP << "*******************************"<<j<<"*******************************"<<std::endl;
            omp_set_dynamic(0);     // Explicitly disable dynamic teams
            omp_set_num_threads(CHANS);
            #pragma omp parallel for 
            for(char i=(CHANS-1); i >=0; --i){
     
                seq_chan[i].play();
            }
     
        }
     
        end2 = std::chrono::steady_clock::now();
     
        std::cout << "///////////////////////////////////////////////////////////////////////"<<std::endl;
     
        double omp_time = std::chrono::duration_cast<std::chrono::microseconds>(end2 - begin2).count();
     
        std::cout << "OMP TIME ELAPSED    = " << omp_time << UNIT << std::endl;
        #endif
     
    	double thread_time = std::chrono::duration_cast<std::chrono::microseconds>(end0 - begin0).count();
    	double seq_time = std::chrono::duration_cast<std::chrono::microseconds>(end1 - begin1).count();
     
     
     
     
    	std::cout << "THREAD TIME ELAPSED = " << thread_time << UNIT << std::endl;
    	std::cout << "SEQUEN TIME ELAPSED = " << seq_time << UNIT << std::endl;
     
    	std::chrono::steady_clock::time_point begin_loop = std::chrono::steady_clock::now();
    	for(int i = 0; i<LOOPS; ++i);
    	std::chrono::steady_clock::time_point end_loop = std::chrono::steady_clock::now();
    	double loop_time = std::chrono::duration_cast<std::chrono::nanoseconds>(end_loop - begin_loop).count();
    	std::cout << "LOOP TIME ELAPSED = " << loop_time << "ns" << std::endl;
     
     
    	return 0;
    }

  18. #18
    Membre chevronné Avatar de Astraya
    Homme Profil pro
    Consommateur de café
    Inscrit en
    Mai 2007
    Messages
    1 043
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 38
    Localisation : France

    Informations professionnelles :
    Activité : Consommateur de café
    Secteur : High Tech - Multimédia et Internet

    Informations forums :
    Inscription : Mai 2007
    Messages : 1 043
    Points : 2 234
    Points
    2 234
    Par défaut
    Bonjour,

    Tout d'abord il faut bien comprendre que le multi-threading n'est pas toujours plus performant sur les processeurs actuels.

    Pour ma part, voici comment j'aurais procédé (libre a toi de tester ou non).
    Déjà, ton code est-il ciblé pour Windows, Linux ou Mac?
    La STL a ses limites et c'est là qu'on les voient, tout d'abord, il faut connaître le nombre de coeur sur ta machine, puis créer n-1 thread correspondant au nombre de coeur, ensuite pour chaque thread, tu lui donne une affinité par coeur + 1 coeur pour le main ( tu sens le task scheduler qui arrive?), et ceci tu ne peux le faire correctement que avec les API système.

    Ensuite, chaque thread à la même logique, voler dans un pile fifo des tâches a exécuter (ici c'est ce que tu fais dans ta boucle). Le input et le résultat ( output) est local à la tâche ( donc au thread -> pas de lock). C'est ce qu'on appelle du Job/work/task stealing.

    Tu synchronises les tâches avec une logique dans le main qui envoi les N tâches pour le temp T dans la fifo, les threads qui sont en attente les exécutent en parallèle.
    Chaque tâches N informe le main (via event, ou autre) que le temp T a un canal de traité, si tout les canaux envoyé au temp T sont traitées, tu additionne les échantillons dans le main ( sans lock nécessaire) et ainsi de suite.
    Tu ne fais aucune allocation dynamique pendant les traitements, les ajouts dans les FIFO ( juste 1 fois au début), tu n'as que 1 seul lock ( la fifo) qui peut être lockfree .

    Voila ce que je ferais ( j'écris sur mon téléphone alors désolé pour les pavés)
    C'est globalement un task/job/work schedulers,

    EDIT: tu peux aussi modifier les tâches pour qu'elles décodent le temp T dans une tâche, un thread fait le temps T, l'autre T+1, et ainsi de suite ( ce serait peut-être même plus rapide que la première version que j'ai donné), en tout cas tu peux tester et comparer facilement avec un job scheduler.
    Homer J. Simpson


  19. #19
    Rédacteur/Modérateur


    Homme Profil pro
    Network game programmer
    Inscrit en
    Juin 2010
    Messages
    7 115
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : Canada

    Informations professionnelles :
    Activité : Network game programmer

    Informations forums :
    Inscription : Juin 2010
    Messages : 7 115
    Points : 32 965
    Points
    32 965
    Billets dans le blog
    4
    Par défaut
    Citation Envoyé par ABD-Z Voir le message
    sur un canal qui dure à peine 1ms, j'ai l'impression que les threads ne sont pas utiles...
    Tu crées des threads pour exécuter des trucs qui durent 1ms ??
    Les threads c'est pas magiques. Et en particulier ça prend du temps à s'initialiser.
    De base, une threadpool sera plus efficace, si ça a du sens pour ton programme d'en utiliser une.
    Ensuite, si tu utilises juste la STD, tu peux pas choisir leur affinité par coeur, donc faut compter sur le système pour faire ça bien. Bof.
    Comme le dit Astraya, il faut utiliser les API spécifiques de chaque plateforme pour y remédier.
    https://docs.microsoft.com/en-us/win...adaffinitymask
    Bref, méconnaissance des threads, mutex et on connait pas ton programme donc... kamoulox.
    Pensez à consulter la FAQ ou les cours et tutoriels de la section C++.
    Un peu de programmation réseau ?
    Aucune aide via MP ne sera dispensée. Merci d'utiliser les forums prévus à cet effet.

  20. #20
    Membre éclairé
    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
    262
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 26
    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 : 262
    Points : 788
    Points
    788
    Billets dans le blog
    2
    Par défaut
    Citation Envoyé par Astraya Voir le message
    Bonjour,
    Chaque tâches N informe le main (via event, ou autre) que le temp T a un canal de traité, si tout les canaux envoyé au temp T sont traitées, tu additionne les échantillons dans le main ( sans lock nécessaire) et ainsi de suite.
    Tu ne fais aucune allocation dynamique pendant les traitements, les ajouts dans les FIFO ( juste 1 fois au début), tu n'as que 1 seul lock ( la fifo) qui peut être lockfree .
    J'ai effectivement pensé à faire ça, c'est à dire laisser chaque Thread calculer tous les samples du buffer pour chaque canal puis les additionner ensemble. Ça nous évite de faire la synchro à chaque seconde.
    C'est vrai que ça en l'air bien plus performant, or je crains que cela compliquerait les choses car un canal peut contenir des effets agissant sur toutes la piste.
    Imaginons que le thread du Canal0 est à l'instant T et le Canal1 bien en avance est à l'instant T+N. À T, on lit l'effet crescendo (augmentation continuelle du volume) sur le Canal0. Or Canal1 est dans le futur et n'est pas affecté par l'effet à l'instant N. On donc (T+N)-T, soit N unités de temps sans crescendo au niveau du Canal1 alors qu'il devrait l'être aussi.
    Ainsi, cette solution n'est guère envisageable.

    J'ai trouvé un moyen d'accélérer tout ça et ainsi profiter du temps réel de nos machines en utilisant les librairies SIMD d'Intel (SSE, AVX, NEON..). Mais bon, je pense que je ne suis pas encore à là d'exploiter ces technos , surtout qu'elles sont très dépendantes de l'architecture. Je voudrais quelque chose de parallélisable, simulant en quelques sorte les canaux des processeurs audio des vieilles machines.
    Sur la MegaDrive par exemple, chaque canal comporte 4 opérateurs FM. J'imagine bien que ces canaux étaient exécuté en même temps et pour chaque sample à l'instant T!

+ Répondre à la discussion
Cette discussion est résolue.
Page 1 sur 2 12 DernièreDernière

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