====== Programmation en C / C++ ======
===== Eléments de programmation multithread =====
Depuis le C++11, manipuler des threads en C++ est devenu plus simple et totalement portable. Voici un diaporama qui présente brièvement des fonctionnalités standards du C++ actuel qui permettent de lancer des tâches sur des threads et les synchroniser: {{ :wiki:multithread_cpp.pdf |}}. Deux couches d'abstraction sont disponibles: la première est simplement une surcouche objet des threads POSIX, le développeur manipule donc explicitement des threads; alors que la seconde est plus abstraite (et donc moins flexible) pour masquer les mécanismes multithread et simplifier le code. Le code des exemples de la présentation sont également disponibles: {{ :wiki:multithread_cpp.tgz |}}.
Par programmation, il existe plusieurs manières d'estimer le nombre de coeurs disponibles pour le processus courant, ce qui permet de déterminer combien de threads il faut créer pour les besoins d'un programme. En voici trois en C / C++: ''std::hardware_concurrency'' (à partir de C++11), ''omp_max_threads'' (OpenMP), ''sched_getaffinity'' (module standard Linux). Le code suivant teste les trois solutions: [[http://fc.isima.fr/~toussain/docWiki/cores_estimator.cpp|cores_estimator.cpp]]. Il semblerait que les approches avec OpenMP et Sched fournissent les bonnes informations, alors que celle du C++ indique le nombre de coeurs de la machine et non celui de votre réservation SLURM.
===== Mesure des temps de calcul =====
Pour mesurer les temps de calcul d'un programme, une bonne solution consiste à utiliser la commande Unix ''time'' qui permet, si on regarde les temps ''user'' et ''system'', d'avoir une mesure précise et reproductible. Cependant, si on souhaite faire la mesure d'une partie de notre programme, il faut forcément toucher à notre code. On peut utiliser la fonction ''clock'' du C ou bien la classe ''chrono'' du nouveau C++, mais ces deux solutions mesurent le temps perçu (l'équivalent du temps ''real'' de la commande ''time''), c'est-à-dire le temps où l'on attend pour avoir le résultat. Dans un environnement multitâche et multi-utilisateur, on ne peut pas se fier au temps perçu pour avoir une mesure précise et surtout reproductible.
==== La solution: getrusage ====
Une bonne solution pour mesurer un temps de calcul par programmation consiste donc à utiliser la fonction ''getrusage'', disponible en standard pour C et C++ sous Linux. L'utilisation de cette fonction nécessite quelques précautions, donc voici un entête qui permet de faire abstraction des détails: [[http://fc.isima.fr/~toussain/docWiki/cpu_measure.hpp|cpu_measure.hpp]]. Prenons un exemple où l'on souhaite mesurer le temps d'exécution d'une fonction:
#include "cpu_measure.hpp"
long fibonacci(unsigned n) {
if (n<2) return n;
return (fibonacci(n-1) + fibonacci(n-2));
}
int main(void) {
auto start = now();
std::cout << "fibonacci(48) = " << fibonacci(48) << std::endl;
auto end = now();
auto duration = end-start;
std::cout << "real " << duration.real << " ms" << std::endl;
std::cout << "user " << duration.user << " ms" << std::endl;
std::cout << "sys " << duration.system << " ms" << std::endl;
}
La fonction ''now'' s'occupe d'appeler ''getrusage'' (qui fournit des mesures de temps depuis le début de l'exécution du programme) et de synthétiser les résultats sous la forme d'une structure contenant les différents temps ''real'', ''user'' et ''system'' comme le fait la commande ''time''. On effectue donc une mesure juste avant l'appel à la fonction à mesurer, une autre juste après, et on fait la différence des temps. Le résultat de ce programme:
real 16080 ms
user 16088 ms
sys 0 ms
Si on lance la commande ''time'' sur notre programme, on obtient des résultats équivalents:
real 0m16.097s
user 0m16.089s
sys 0m0.004s
==== Temps d'exécution d'un thread ====
La fonction ''getrusage'' dispose d'un paramètre ''who'' qui peut prendre trois valeurs qui affectent la manière de mesurer le temps: ''RUSAGE_SELF'' qui mesure le temps d'exécution du processus courant et de tous ces threads, ''RUSAGE_THREAD'' qui mesure le temps d'exécution du thread courant, et ''RUSAGE_CHILDREN'' qui mesure le temps d'exécution des processus fils du processus courant. Dans l'entête que nous proposons, il est possible d'indiquer l'une des trois valeurs à la fonction ''now'' (qui la transmettra à ''getrusage''). Nous avons choisi ''RUSAGE_SELF'' par défaut, ce qui signifie que si on lance une tâche de 3 secondes sur 4 threads, on obtient un temps ''user'' de 12 secondes (le cumul de tous les threads). Voici un exemple où l'on mesure le temps par thread et le temps du processus:
#include
#include
#include "cpu_measure.hpp"
std::mutex mutex;
long fibonacci(unsigned n) {
if (n<2) return n;
return (fibonacci(n-1) + fibonacci(n-2));
}
void measure(unsigned n) {
auto start = now(RUSAGE_THREAD);
fibonacci(48);
auto end = now(RUSAGE_THREAD);
auto duration = end-start;
mutex.lock();
std::cout << "[thread #" << (n+1) << "]" << std::endl << duration << std::endl;
mutex.unlock();
}
int main(void) {
std::thread t[4];
auto start = now();
for (unsigned i = 0; i<4; ++i) t[i] = std::thread(measure,i);
for (unsigned i = 0; i<4; ++i) t[i].join();
auto end = now();
auto duration = end-start;
mutex.lock();
std::cout << "[overall] " << std::endl << duration << std::endl;
mutex.unlock();
}
Et voici les mesures que l'on peut obtenir, où l'on constate que le temps cumulé des threads correspond bien au temps mesuré pour le processus:
[thread #2]
real = 70480 ms
user = 17622 ms
sys = 0 ms
[thread #1]
real = 70500 ms
user = 17627 ms
sys = 0 ms
[thread #3]
real = 70520 ms
user = 17638 ms
sys = 0 ms
[thread #4]
real = 70520 ms
user = 17637 ms
sys = 2 ms
[overall]
real = 70520 ms
user = 70525 ms
sys = 4 ms