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.
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 #define CHANS 2 #define COUNT 10 #define LOOPS 666
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
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;
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.
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(); } }
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é.
Schéma illustrant la synchronisationEh 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é.
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(); } }
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 !
Partager