UCL-INGI / LEPL1503-Blog

Blog du projet P3 à l'UCLouvain
MIT License
0 stars 10 forks source link

Mesure de temps plus simples et recherche du nombre de threads optimal #20

Closed Eliot-P closed 4 years ago

Eliot-P commented 4 years ago

Bonjour Monsieur,

Suite au récents articles publié sur le blog, j'ai écris un article sur certaines manières de prendre des mesures de temps plus simples ainsi qu'une manière de représenter les temps d’exécutions sous python. Voici la proposition d'article.

Merci d'avance de votre réponse Eliot Peeters

Eliot-P commented 4 years ago

Mesure de performance et optimisation du nombre de threads

Mesure de temps

Suite au récent post sur les mesures de temps en code C, voici le résultat de mes recherches personelles : Il y a plusieurs manières assez simple pour mesurer le temps pris par une foncion trouvable assez facilement sur internet, cependant elles ne sont pas toutes adaptées pour du multithreading. Les méthodes décritent ici font partie du module

Mesure de temps avec clock(3P)

Prennons comme exemple ce code :

double *timer_1(){
    double time_taken;
    clock_t start,end;
    start = clock();
    fun();
    end = clock();
    time_taken =1000*((double)(end - start) / (double)(CLOCKS_PER_SEC));
    return time_taken;
}

La fonction clock(3P) permet de mesurer le temps d'utilisation du processeur. Pour pouvoir obtenir un résultat en seconde il faut donc diviser le résultat par CLOCKS_PER_SEC. Bien que cette fonction soit interessante pour des programmes sans mutlithreading, celle-ci ne lira pas des valeurs correctes pour un programme multithread. En effet puisqu'un programme multithread utilise plusieurs coeur d'un processeur en même temps, c'est du temps processeur en plus alors que le temps réel, lui, reste le même.

Mesure de temps avec time(3P)

Prennons comme exemple ce code :

double timer_2(){
    time_t start,end;
    start = time(NULL);
    fun();
    end = time(NULL);
    return (double)(end - start);
}

La fonction time(3P) nous retourne le temps écoulé en seconde depuis "l'époque". L'époque est une date fixée, pour linux il s'agit du 1er janvier 1970 à 00h 00m 00s GMT (fun-fact, il s'agit de la date du début de l'ère UNIX). Il nous suffit ensuite de faire la différence entre end et start afin de trouver le temps écoulé. Bien que cette fonction lise des valeurs totalement correctes pour un programme singlethread et multithread, il peut être interessant de vouloir une précision supplémentaire en millisecondes.

Mesure de temps avec gettimeofday(3P)

Prennons comme exemple ce code :

double timer_3(){
    struct timeval start,end;
    gettimeofday(&start, NULL) ;
    fun();
    gettimeofday(&end,NULL);
    double time_taken; 
    time_taken = (end.tv_sec - start.tv_sec) * 1e6; 
    time_taken = (time_taken + (end.tv_usec -  start.tv_usec)) * 1e-6; 
    return time_taken;
}

la fonction gettimeofday(3P) nous retourne une lecture précise de la date et de l'heure actuelle. Il nous suffit de nouveau donc de faire une difference entre les seconde et les micro secondes de end et start pour avoir le temps écoulé. Pour effectuer cette différence nous utilison les attributs .tv_sec et .tv_usec de la structure timeval décrite dans <sys/time.h>.

Optimisation du nombre de threads

Afin de trouver le nombre de threads optimal, il nous faudrait executer plusieurs fois le programme avec un nombre de threads différent à chaque execution. Cependant avec un même nombre de thread, le temps d'execution peut varier. Il est donc interessant d'executer le programme X fois pour n threads et ceci de 1 à N threads.

Pour pouvoir mettre tout ces résultats dans un joli graphique et pouvoir inclure ce dernier dans le rapport, une solution assez évidente est d'utiliser python et ses librairies matplotlib et numpy.

Prérequis

  • Utiliser Linux
  • Installer la librairie py-cpuinfo (pas obligatoire)
  • Avoir executable du programme de la forme ./fact [-N nombre_de_threads] fichier_input fichier_output
  • fact imprime UNIQUEMENT le temps en micro-seconde dans le terminal

Les imports

import os
from numpy import *
import matplotlib.pyplot as plt
import cpuinfo

On écrit premièrement une fonction qui prend comme argument le nombre de thread et qui retourne le temps pris en micro-secondes au moyen de la librairie os qui est comprise de base dans python :

def exec (Number_of_thread) : 
    execution = os.popen('./fact -N ' + str(Number_of_thread) + 'Input.txt Output.txt') #écrit dans le terminal
    time_taken_raw = execution.read() #lit ce que l'execution écris dans le terminal
    time_taken = float(time_taken_raw.strip()) #transforme en float et retire le "\n"
    os.system('rm -rf Output.txt')
    return time_taken

On écrit ensuite une fonction qui prend pour arguments le nombre de threads maximum et le nombre d'exéctuions du programme pour n threads :

def main (number_of_exec,max_number_of_thread):
    big_array = []
    for n in range (max_number_of_thread):
        array_of_time_for_n_thread = []
        for i in range (number_of_exec):
            time_taken = around(exec(max_number_of_thread)*100,2)
            array_of_time_for_n_thread.append(time_taken)
        big_array.append(array_of_time_for_n_thread)
    grapher(big_array)

Et finalement il ne nous reste plus qu'à écrire la fonction [grapher()]() qui prend pour argument l'array des temps d'executions et retourne un graphique. Il s'agit uniquement de fonctions matplotlib et numpy de base, je n'entrerais donc pas dans les détails sur cette fonction

def grapher(array):
    n = 1
    ax = plt.subplot()
    arr = asarray(array)
    mean_arr = []
    for n_thread in arr : 
        maxi = amax(n_thread)
        mini = amin(n_thread)
        moy = mean(n_thread)
        ax.plot(n,maxi,'go',)
        ax.text(n-0.05,maxi,maxi)
        ax.plot(n,mini,'go')
        ax.text(n-0.05,mini,mini)
        ax.plot([n,n],[mini,maxi],'g')
        ax.plot(n,moy,'bo')
        mean_arr.append(moy)
        n+=1
    mean_numpy_arr = asarray(mean_arr)
    ax.plot(range(1,n),mean_numpy_arr,'b--')
    ax.axhline(amin(mean_numpy_arr))
    ax.set_xlabel('Number of thread [N]')
    ax.set_ylabel('Time [ms]')
    ax.set_title("Execution de fact avec l'exemple d'input sur un processeur " + cpuinfo.get_cpu_info()['brand'].split("w/")[0] ,pad=30)
    #l'appel à cpuinfo.get_cpu_info()['brand'].split("w/")[0] permet d'avoir le nom du processeur
    plt.show()

Et voilà après en entrant par exemple cette commande dans le programme python :

main(10, 8)

Voici le graphique obtenu. En vert on peut voir le temps d execution maximum et minimum observés lors des X exéctuions pour n threads et en bleu la moyenne du temps mis pour n threads. Enfin en rouge on peut voir le minimum de la moyenne du graph.

Graph

obonaventure commented 4 years ago

Bonjour,

Pour la première partie, gettimeofday est déjà couvert dans l'autre blog post et je ne recommanderais pas clock ou time dans le cadre de ce tp.

La seconde partie du post me semble un bon point de départ. Ce serait bien de pouvoir lancer le programme fact depuis python directement, c'est possible mais l'API a changé dans python3 et il faut faire attention à ce que l'on fait.

Pour la collecte des mesures, on pourrait aussi mesurer la déviation standard ou essayer de reproduire les mesures jusqu'à ce qu'elles soient stables (normalement elles devraient l'être puisque le calcul est déterministe).

OB

Eliot-P commented 4 years ago

Bonjour, voici une version retravaillée de l'article. J'ai essayé d'y appliquer vos commentaires.

Bonne Journée Eliot Peeters

Eliot-P commented 4 years ago

Optimisation du nombre de threads au moyen de Python

Afin de trouver le nombre de threads optimal, il nous faudrait exécuter plusieurs fois le programme avec un nombre de threads différent à chaque exécution. Cependant avec un même nombre de thread, le temps d'exécution peut varier. Il est donc interessant d'exécuter le programme X fois pour n threads et ceci de 1 à N threads.

Pour pouvoir mettre tout ces résultats dans un joli graphique et pouvoir inclure ce dernier dans le rapport, une solution assez évidente est d'utiliser python et ses librairies matplotlib et numpy.

Les Librairies Python

os numpy matplotlib py-cpuinfo (pas obligatoire) time ctypes

Les imports dans le programme

import os
from numpy import *
import matplotlib.pyplot as plt
import cpuinfo
from time import time
from ctypes import *

Pour exécuter notre code C depuis python, il y a deux manières possibles, soit exécuter des commandes shell depuis python, soit exécuter les fonctions du programme C directement depuis python.

Exécution version shell

Pour cette version, le principe est d'exécuter le programme C dans le terminal au moyen de python. Donc pour peu que l'exectuable soit de la forme ./fact [-N nombre_de_threads] fichier_input fichier_output et qu'il écrive uniquement le temps en microsecondes dans le terminal cette verion fonctionnera. On écrit premièrement une fonction qui prend comme argument le nombre de threads et qui retourne le temps pris en micro-secondes au moyen de la librairie os qui est comprise de base dans python :

def exec (Number_of_thread) : 
    execution = os.popen('./fact -N {} Input.txt Output.txt'.format(str(Number_of_thread))) #écrit dans le terminal
    time_taken_raw = execution.read() #lit ce que l'execution écris dans le terminal
    try :
        time_taken = float(time_taken_raw.strip())*1000#transforme en float et retire le "\n"
    except:
        time_taken = -1
    return time_taken

Exectuion version C

¨Pour cette version, le principe est d'exécuter une des fonctions du code C directement dans le code Python. Pour ce faire, il nous faut d'abord créer une librairie partagée au moyen de gcc(1) en entrant la commande suivante dans le terminal :

gcc -fPIC -o fact.so fact.c -lpthread

Une fois cette librairie créée, nous pouvos directement l'importer dans python au moyen de ces lignes définissant les variables globales dans notre programme :

so_file = "/Chemin_vers_le_fichier/fact.so"
fact = CDLL(so_file)

Maintenant, il est possible d'appeler n'importe quel fonction de programme C dans python au moyen de la ligne de code :

fact.fun()

Un exemple de la fonction exec serait alors :

def exec_2 (Number_of_thread) : 
    t1 = time()
    fact.main(Number_of_thread)
    t2 = time()
    time_taken = t2 - t1
    return time_taken

Il est à noter qu'ici le temps est directement mesurer en Python car passer par gettimeofday(3P) ne nous lit pas des valeurs correctes lorsque celle-ci est executée depuis Python. Enfin il faut faire bien attention que la fonction main ai bien fini son exécution avant de passer à l'exécution suivante afin d'éviter des soucis entre les threads. Pour ceci il peut être interresant de créer une fonction auxiliaire comme décrite dans le post sur le blog concernant les mesures de temps.

Reste du Programme

On écrit maintenant une fonction qui prend pour arguments le nombre de threads maximum et le nombre d'exéctuions du programme pour n threads :

def main (number_of_exec,max_number_of_thread):
    big_array = []
    n_error = 0
    for n in range (max_number_of_thread):
        array_of_time_for_n_thread = []
        for i in range (number_of_exec):
            time_taken = exec(max_number_of_thread)
            if time_taken != -1 :
                array_of_time_for_n_thread.append(time_taken)
            else : 
                n_error += 1
        big_array.append(array_of_time_for_n_thread)
    grapher(big_array,n_error)

Et finalement il ne nous reste plus qu'à écrire la fonction [grapher()]() qui prend pour argument l'array des temps d'executioné et retourne un graphique. Il s'agit uniquement de fonctions matplotlib et numpy de base, je n'entrerais donc pas dans les détails sur cette fonction

def grapher(array,n_error):
    n = 1
    ax = plt.subplot()
    arr = asarray(array)
    mean_arr = []
    for n_thread in arr : 
        stand = around(std(n_thread),2)
        maxi = around(amax(n_thread),2)
        mini = around(amin(n_thread),2)
        moy = around(mean(n_thread),2)
        ax.plot(n,maxi,'go',)
        ax.plot(n,mini,'go')
        ax.plot([n,n],[mini,maxi],'g')
        ax.plot(n,moy,'bo')
        ax.text(n + 0.09,moy,"$\sigma={}$\nmoy={}\nmax={}\nmin={}".format(stand,moy,maxi,mini),bbox=dict(facecolor='wheat', alpha=0.7))
        mean_arr.append(moy)
        n+=1
    mean_numpy_arr = asarray(mean_arr)
    ax.plot(range(1,n),mean_numpy_arr,'b--')
    ax.axhline(amin(mean_numpy_arr))
    ax.set_xlabel('Number of thread [N]')
    ax.set_ylabel('Time [ms]')
    ax.text(0.05, 0.1, '{} ERREURS'.format(n_error), fontsize=10, transform=plt.gcf().transFigure)
    ax.set_title("Execuéion de fact avec l'exemple d'input sur un processeur " + cpuinfo.get_cpu_info()['brand'].split("w/")[0] ,pad=30)
    #l'appel à cpuinfo.get_cpu_info()['brand'].split("w/")[0] permet d'avoir le nom du processeur
    plt.show()

Et voilà après en entrant par exemple cette commande dans le programme python :

main(5, 8)

Voici le graphique obtenu. En vert on peut voir le temps d executioé maximum et minimum observés lors des X exéctuions pour n threads et en bleu la moyenne du temps mis pour n threads, et en rouge on peut voir le minimum de la moyenne du graph. Enfin dans chaque cadre nous pouvons voir les valeurs de la déviation standard, la moyenne, le maximum ainsi que le minimum. Dans le coin en bas à gauche, il y a le nombre d'erreurs encontrée lors de l'executionédu programme fact si jamais il vennait à y en avoir.

Graph

obonaventure commented 4 years ago

Je propose de le migrer vers une pull request. Il y a quelque typos qu'on règlera dans la pull request. Pour l'insertion d'une librairie dans le code python, cela vaudrait la peine d'ajouter une référence vers un document sur le web