Coder un timer pour son programme multithreadé

Le but de ce post est de voir comment nous pouvons construire un programme qui va en lancer un autre, plusieurs fois, en passant en argument de ce programme un nombre de threads croissants. De cette manière, une simple commande dans le terminal permet de lancer son programme plusieurs fois, avec le nombre de threads en argument allant de 1 à 10, par exemple.

Pratiquement, ce programme va appeler votre exécutable avec 1 thread, puis 2, puis 3 … jusque 10 (voir plus, à votre guise) et afficher le temps d’exécution correspondant.

Le code que nous allons écrire fera appel à quelques notions encore non vues, mais le but est de vous fournir un outil que vous pourrez utiliser pour automatiser les tests de performances que vous ferrez dans le cadre du cours LEPL1503.

Si vous ne voulez pas vous ennuyer à lire les explications, la version du code en entier est disponible à la fin de ce post.

C’est parti !

Tout d’abord, les imports:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/time.h>
#include <time.h>
#include <libgen.h>
#include <unistd.h>
#include <stdint.h>
#include <math.h>

NOTE Il faudra faire compiler ce fichier avec le drapeau -lm pour l’utilisation de la librairie math.h. On enchaîne maintenant avec la fonction main ainsi que les variables globales :

int substract_timeval(struct timeval* result, struct timeval *x, struct timeval* y);
double execute(int i);
void execute_fils(int i);
void save_csv(char* filename, double *temps, int th);

char* filename_cache = "in_out/cache.txt";
char* filename_in = "in_out/example_input.txt";
char* filename_out = "in_out/example_output.txt";
char* filename_csv = "in_out/output.csv";

int main(int argc, char** argv){
    int th = 10, i;
    double temps[th];

    //Lancement de l'exécution de mise en cache
    execute(0);

    for(i=1; i<=th; i++){
        double ti = execute(i);
        temps[i-1] = ti;
        sleep(200);
    }

    for(i=0; i < th; i++){
        printf("Temps mis pour %d threads : %f\n", i+1, temps[i]);
    }

    save_csv(filename_csv, temps, th);

    printf("\n");
    return EXIT_SUCCESS;
}

Ici, la première variable va servir à pouvoir mesurer le temps tandis que les deux lignes suivantes servent juste à déclarer les deux méthodes auxiliaires dont nous aurons besoin plus tard. Description des variables :

Variable Utilité
int th nombre de threads max à lancer, doit avoir la valeur 1 au minimum
double temps[th] tableau servant à conserver le temps d’exécution pour chacune des exécutions

L’appel à la méthode execute_cache, dont le code sera détaillé par la suite, nous sert à charger une première fois l’exécutable dans le cache. Si nous ne faisions pas cet appel, la première exécution serait plus lente que les autres. Pour cela, il faut qu’un fichier d’input situé au chemin in_out/cache.txt. L’appel de mise en cache se fera sur ce fichier qui pourra être plus court que celui que vous voulez tester, se trouvant au chemin in_out/example_input.txt quant à lui.

La première boucle for s’occupe de lancer les th threads. La fonction execute retourne le temps pris par l’exécution et nous stockons cette info dans le tableau temps.

La seconde boucle for sert simplement à imprimer à l’écran les temps d’exécution précédemment calculés.

L’appel système sleep permet de patienter un court instant entre deux exécution de l’exécutable fact. Le nombre en argument est le temps d’attente en millisecondes pour les système Linux, et en secondes pour les systèmes Windows.

Voici le code de la méthode suivante :

double execute(int i){
    struct timeval start, stop, temps;
    int status = 0;
    gettimeofday(&start, NULL);
    pid_t pid = fork();
    if(pid == 0){ //fils
        execute_fils(i);
    } else if (pid < 0) { // Erreur lors de la création du processus fils
       printf("Erreur lors de l'exécution avec %d threads (fork).", i);
       return 0.0;
    } else { // pere
        int fils = waitpid(pid, &status, 0);
        gettimeofday(&stop, NULL);
        substract_timeval(&temps, &stop, &start);
        if(fils == -1){ exit(EXIT_FAILURE);}
        double s = (double) temps.tv_sec;
        double us = ((double) temps.tv_usec) / 1000000;
        return  (s + us);
    }
    return -1.0;
}

Cette méthode lance l’exécution du programme avec i threads, nombre passé en argument.

Ici, la partie subtile est l’utilisation de l’appel système fork qui permet de lancer un nouveau processus. Je ne rentre pas dans les détails, mais chaque processus possède un PID qui permet à l’OS de les distinguer. La méthode fork retourne soit 0 si le processus dans lequel nous sommes est celui nouvellement créé, soit le PID du processus nouvellement créé si nous nous trouvons dans l’ancien processus, appelé le “processus père”.

Dès lors, si la valeur que fork retourne est 0, nous sommes le processus nouvellement créé qui doit exécuter notre programme multithreadé. Si elle retourne autre chose, nous nous trouvons dans le processus père qui doit juste attendre que le fils ait fini son exécution.

La méthode waitpid attend la fin du processus ayant pour PID celui spécifié en argument. La variable status est là où on va pouvoir stocker la valeur de retour du processus qui se finit. Le troisème argument peut être laissé à 0.

Les deux appels gettimeofday servent à la mesure du temps d’exécution, ce temps étant la valeur de retour de la fonction. Il est important de noter que l’utilisation de cet appel donne une estimation du temps d’exécution, mais pas le temps exact. Néanmoins, il nous permettra de mettre en avant l’efficacité des threads dans votre programme.

Enfin, nous passons à la dernière méthode principale de ce programme :

// Si i > 0, exécution normale avec i threads de calcul
// Si i == 0, exécution de mise en cache avec 8 threads
void execute_fils(int i) {
    char* env[] = {"LANG=fr", NULL};
    char *arg[7];
    arg[0] = "./fact";
    if (i > 0) {
        arg[1] = filename_in;
    } else { //Si exécution de mise en cache
        arg[1] = filename_cache;
    }
    arg[2] = filename_out;
    arg[3] = "-N";
    char tab[3];
    arg[4] = tab;
    arg[5] = "-q";
    arg[6] = NULL;

    //Calcul de la longueur du nombre i lors
    //de sa conversion en string
    if (i > 0) {
        size_t size = ((int) log10(i) + 2) * sizeof(char);
        snprintf(arg[4], size, "%d", i);
        printf("Je lance %s avec %s threads !\n", arg[0], arg[4]);
    } else {
        snprintf(arg[4], 2*sizeof(char), "%d", 8);
        printf("Je lance %s pour la mise en cache avec %s threads!\n", arg[0], arg[4]);
    }

    //Lancement de l'exécutable
    execve(arg[0], arg, env);
}

Ici, on va lancer le nouveau processus avec l’appel système execve. Le premier paramètre est le nom de l’exécutable (./fact dans le cadre de ce cours), le second la liste des arguments à passer à l’exécutable (avec le nom de l’exécutable en première position), et le troisième est l’environnement d’exécution, que vous pouvez laisser à la même valeur que dans le code ci-dessus.

Notons que la taille du tableau à l’adresse arg[4] a une longueur de 3. En tenant compte du caractère ‘\0’, nous pouvons y inscrire des nombres allant de 1 à 99 maximum. Cela veut dire que sans modification du programme de votre part, la valeur de th dans la fonction main devra être comprise entre 1 et 99.

Enfin, nous avons besoin de deux méthodes auxilliaires pour la mesure du temps d’exécution, ainsi que pour la sauvegarde de ces temps dans un fichier au format csv:

int substract_timeval(struct timeval* result, struct timeval *x, struct timeval* y) {
    if (x->tv_usec < y->tv_usec) {
        int nsec = (y->tv_usec - x->tv_usec) / 1000000 + 1;
        y->tv_usec -= 1000000 * nsec;
        y->tv_sec += nsec;
    }
    if (x->tv_usec - y->tv_usec > 1000000) {
        int nsec = (x->tv_usec - y->tv_usec) / 1000000;
        y->tv_usec += 1000000 * nsec;
        y->tv_sec -= nsec;
    }
    result->tv_sec = x->tv_sec - y->tv_sec;
    result->tv_usec = x->tv_usec - y->tv_usec;
    
    return x->tv_sec < y->tv_sec;
}
void save_csv(char* filename, double *temps, int th) {
    FILE *fd = fopen(filename, "w");
    if (fd == NULL) {
        printf("Erreur lors de l'ouvberture du fichier csv (fopen)");
    }
    
    fprintf(fd, "%s,%s\n", "Nombre de threads", "temps");

    int i;
    for(i = 0; i<th; i++) {
        fprintf(fd, "%d,%f\n", i+1, temps[i]);
    }
    fclose(fd);
}

####Code entier

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/time.h>
#include <time.h>
#include <libgen.h>
#include <unistd.h>
#include <stdint.h>
#include <math.h>

int substract_timeval(struct timeval* result, struct timeval *x, struct timeval* y);
double execute(int i);
void execute_fils(int i);
void save_csv(char* filename, double *temps, int th);

char* filename_cache = "in_out/cache.txt";
char* filename_in = "in_out/example_input.txt";
char* filename_out = "in_out/example_output.txt";
char* filename_csv = "in_out/output.csv";

int main(int argc, char** argv){
    int th = 10, i;
    double temps[th];

    //Lancement de l'exécution de mise en cache
    execute(0);

    for(i=1; i<=th; i++){
        double ti = execute(i);
        temps[i-1] = ti;
        sleep(200);
    }

    for(i=0; i < th; i++){
        printf("Temps mis pour %d threads : %f\n", i+1, temps[i]);
    }

    save_csv(filename_csv, temps, th);

    printf("\n");
    return EXIT_SUCCESS;
}

double execute(int i){
    struct timeval start, stop, temps;
    int status = 0;
    gettimeofday(&start, NULL);
    pid_t pid = fork();
    if(pid == 0){ //fils
        execute_fils(i);
    } else if (pid < 0) { // Erreur lors de la création du processus fils
       printf("Erreur lors de l'exécution avec %d threads (fork).", i);
       return 0.0;
    } else { // pere
        int fils = waitpid(pid, &status, 0);
        gettimeofday(&stop, NULL);
        substract_timeval(&temps, &stop, &start);
        if(fils == -1){ exit(EXIT_FAILURE);}
        double s = (double) temps.tv_sec;
        double us = ((double) temps.tv_usec) / 1000000;
        return  (s + us);
    }
    return -1.0;
}

// Si i > 0, exécution normale avec i threads de calcul
// Si i == 0, exécution de mise en cache avec 8 threads
void execute_fils(int i) {
    char* env[] = {"LANG=fr", NULL};
    char *arg[7];
    arg[0] = "./fact";
    if (i > 0) {
        arg[1] = filename_in;
    } else { //Si exécution de mise en cache
        arg[1] = filename_cache;
    }
    arg[2] = filename_out;
    arg[3] = "-N";
    char tab[3];
    arg[4] = tab;
    arg[5] = "-q";
    arg[6] = NULL;

    //Calcul de la longueur du nombre i lors
    //de sa conversion en string
    if (i > 0) {
        size_t size = ((int) log10(i) + 2) * sizeof(char);
        snprintf(arg[4], size, "%d", i);
        printf("Je lance %s avec %s threads !\n", arg[0], arg[4]);
    } else {
        snprintf(arg[4], 2*sizeof(char), "%d", 8);
        printf("Je lance %s pour la mise en cache avec %s threads!\n", arg[0], arg[4]);
    }

    //Lancement de l'exécutable
    execve(arg[0], arg, env);
}


int substract_timeval(struct timeval* result, struct timeval *x, struct timeval* y) {
    if (x->tv_usec < y->tv_usec) {
        int nsec = (y->tv_usec - x->tv_usec) / 1000000 + 1;
        y->tv_usec -= 1000000 * nsec;
        y->tv_sec += nsec;
    }
    if (x->tv_usec - y->tv_usec > 1000000) {
        int nsec = (x->tv_usec - y->tv_usec) / 1000000;
        y->tv_usec += 1000000 * nsec;
        y->tv_sec -= nsec;
    }
    result->tv_sec = x->tv_sec - y->tv_sec;
    result->tv_usec = x->tv_usec - y->tv_usec;
    
    return x->tv_sec < y->tv_sec;
}

void save_csv(char* filename, double *temps, int th) {
    FILE *fd = fopen(filename, "w");
    if (fd == NULL) {
        printf("Erreur lors de l'ouvberture du fichier csv (fopen)");
    }
    
    fprintf(fd, "%s,%s\n", "Nombre de threads", "temps");

    int i;
    for(i = 0; i<th; i++) {
        fprintf(fd, "%d,%f\n", i+1, temps[i]);
    }
    fclose(fd);
}


Ecrit le April 24, 2020