Maison >développement back-end >C++ >La Quête de la Performance Partie III : C Force

La Quête de la Performance Partie III : C Force

王林
王林original
2024-08-06 01:10:021217parcourir

The Quest for Performance Part III : C Force

Dans les deux volets précédents de cette série, nous avons examiné les performances des opérations flottantes en Perl,
Python et R dans un exemple de jouet qui a calculé la fonction cos(sin(sqrt(x))), où x était un très grand tableau de 50 millions de nombres flottants double précision.
Les implémentations hybrides qui déléguaient la partie arithmétique intensive à C étaient parmi les implémentations les plus performantes. Dans cet article, nous allons faire une légère digression et examiner les performances d'une implémentation en code C pur de l'exemple de jouet.
Le code C fournira des informations supplémentaires sur l'importance de la localité mémoire pour les performances (par défaut, les éléments d'un tableau C sont stockés dans des adresses séquentielles en mémoire, et les API numériques telles que PDL ou l'interface numpy avec de tels conteneurs) par rapport aux conteneurs. ,
par ex. Tableaux Perl qui ne stockent pas leurs valeurs dans des adresses séquentielles en mémoire. Dernier point, mais non le moindre, les implémentations du code C nous permettront d'évaluer si les indicateurs liés aux opérations en virgule flottante pour le compilateur de bas niveau (dans ce cas gcc) peuvent affecter les performances.
Ce point mérite d'être souligné : le commun des mortels dépend entièrement du choix des indicateurs du compilateur lors du "piping" de son "installation" ou de la construction de son fichier Inline. Si l’on ne touche pas ces drapeaux, on ignorera parfaitement ce qu’ils pourraient manquer ou les pièges qu’ils pourraient éviter.
Le modeste makefile du fichier C permet de faire explicitement de telles évaluations de performances.

Le code C de notre exemple de jouet est répertorié dans son intégralité ci-dessous. Le code est plutôt explicite, je ne passerai donc pas de temps à expliquer autre que souligner qu'il contient quatre fonctions pour

  • Calcul non séquentiel de la fonction coûteuse : les trois opérations de pointage flottant s'effectuent dans une seule boucle à l'aide d'un seul thread
  • Calculs séquentiels de la fonction coûteuse : chacune des 3 évaluations de fonctions à virgule flottante se déroule dans une boucle séparée à l'aide d'un seul thread
  • Code OpenMP non séquentiel : version threadée du code non séquentiel
  • Code OpenMP séquentiel : thread du code séquentiel

Dans ce cas, on peut espérer que le compilateur soit suffisamment intelligent pour reconnaître que la racine carrée correspond à des opérations de virgule flottante compressées (vectorisées) dans l'assembly, de sorte qu'une fonction puisse être vectorisée à l'aide des instructions SIMD appropriées (notez que nous l'avons fait n'utilisez pas le programme simd pour les codes OpenMP).
Peut-être que l'accélération de la vectorisation pourrait compenser la perte de performances liée à l'accès répété aux mêmes emplacements mémoire (ou non).

#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <stdio.h>
#include <omp.h>

// simulates a large array of random numbers
double*  simulate_array(int num_of_elements,int seed);
// OMP environment functions
void _set_openmp_schedule_from_env();
void _set_num_threads_from_env();



// functions to modify C arrays 
void map_c_array(double* array, int len);
void map_c_array_sequential(double* array, int len);
void map_C_array_using_OMP(double* array, int len);
void map_C_array_sequential_using_OMP(double* array, int len);

int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("Usage: %s <array_size>\n", argv[0]);
        return 1;
    }

    int array_size = atoi(argv[1]);
    // printf the array size
    printf("Array size: %d\n", array_size);
    double *array = simulate_array(array_size, 1234);

    // Set OMP environment
    _set_openmp_schedule_from_env();
    _set_num_threads_from_env();

    // Perform calculations and collect timing data
    double start_time, end_time, elapsed_time;
    // Non-Sequential calculation
    start_time = omp_get_wtime();
    map_c_array(array, array_size);
    end_time = omp_get_wtime();
    elapsed_time = end_time - start_time;
    printf("Non-sequential calculation time: %f seconds\n", elapsed_time);
    free(array);

    // Sequential calculation
    array = simulate_array(array_size, 1234);
    start_time = omp_get_wtime();
    map_c_array_sequential(array, array_size);
    end_time = omp_get_wtime();
    elapsed_time = end_time - start_time;
    printf("Sequential calculation time: %f seconds\n", elapsed_time);
    free(array);

    array = simulate_array(array_size, 1234);
    // Parallel calculation using OMP
    start_time = omp_get_wtime();
    map_C_array_using_OMP(array, array_size);
    end_time = omp_get_wtime();
    elapsed_time = end_time - start_time;
    printf("Parallel calculation using OMP time: %f seconds\n", elapsed_time);
    free(array);

    // Sequential calculation using OMP
    array = simulate_array(array_size, 1234);
    start_time = omp_get_wtime();
    map_C_array_sequential_using_OMP(array, array_size);
    end_time = omp_get_wtime();
    elapsed_time = end_time - start_time;
    printf("Sequential calculation using OMP time: %f seconds\n", elapsed_time);

    free(array);
    return 0;
}



/*
*******************************************************************************
* OMP environment functions
*******************************************************************************
*/
void _set_openmp_schedule_from_env() {
  char *schedule_env = getenv("OMP_SCHEDULE");
  printf("Schedule from env %s\n", getenv("OMP_SCHEDULE"));
  if (schedule_env != NULL) {
    char *kind_str = strtok(schedule_env, ",");
    char *chunk_size_str = strtok(NULL, ",");

    omp_sched_t kind;
    if (strcmp(kind_str, "static") == 0) {
      kind = omp_sched_static;
    } else if (strcmp(kind_str, "dynamic") == 0) {
      kind = omp_sched_dynamic;
    } else if (strcmp(kind_str, "guided") == 0) {
      kind = omp_sched_guided;
    } else {
      kind = omp_sched_auto;
    }
    int chunk_size = atoi(chunk_size_str);
    omp_set_schedule(kind, chunk_size);
  }
}

void _set_num_threads_from_env() {
  char *num = getenv("OMP_NUM_THREADS");
  printf("Number of threads = %s from within C\n", num);
  omp_set_num_threads(atoi(num));
}
/*
*******************************************************************************
* Functions that modify C arrays whose address is passed from Perl in C
*******************************************************************************
*/

double*  simulate_array(int num_of_elements, int seed) {
  srand(seed); // Seed the random number generator
  double *array = (double *)malloc(num_of_elements * sizeof(double));
  for (int i = 0; i < num_of_elements; i++) {
    array[i] =
        (double)rand() / RAND_MAX; // Generate a random double between 0 and 1
  }
  return array;
}

void map_c_array(double *array, int len) {
  for (int i = 0; i < len; i++) {
    array[i] = cos(sin(sqrt(array[i])));
  }
}

void map_c_array_sequential(double* array, int len) {
  for (int i = 0; i < len; i++) {
    array[i] = sqrt(array[i]);
  }
  for (int i = 0; i < len; i++) {
    array[i] = sin(array[i]);
  }
  for (int i = 0; i < len; i++) {
    array[i] = cos(array[i]);
  }
}

void map_C_array_using_OMP(double* array, int len) {
#pragma omp parallel
  {
#pragma omp for schedule(runtime) nowait
    for (int i = 0; i < len; i++) {
      array[i] = cos(sin(sqrt(array[i])));
    }
  }
}

void map_C_array_sequential_using_OMP(double* array, int len) {
#pragma omp parallel
  {
#pragma omp for schedule(runtime) nowait
    for (int i = 0; i < len; i++) {
      array[i] = sqrt(array[i]);
    }
#pragma omp for schedule(runtime) nowait
    for (int i = 0; i < len; i++) {
      array[i] = sin(array[i]);
    }
#pragma omp for schedule(runtime) nowait
    for (int i = 0; i < len; i++) {
      array[i] = cos(array[i]);
    }
  }
}

Une question cruciale est de savoir si l'utilisation d'indicateurs de compilateur flottants rapides, une astuce qui échange la vitesse contre la précision du code, peut affecter les performances.
Voici le makefile sans ce drapeau du compilateur

CC = gcc
CFLAGS = -O3 -ftree-vectorize  -march=native  -Wall -std=gnu11 -fopenmp -fstrict-aliasing 
LDFLAGS = -fPIE -fopenmp
LIBS =  -lm

SOURCES = inplace_array_mod_with_OpenMP.c
OBJECTS = $(SOURCES:.c=_noffmath_gcc.o)
EXECUTABLE = inplace_array_mod_with_OpenMP_noffmath_gcc

all: $(SOURCES) $(EXECUTABLE)

clean:
    rm -f $(OBJECTS) $(EXECUTABLE)

$(EXECUTABLE): $(OBJECTS)
    $(CC) $(LDFLAGS) $(OBJECTS) $(LIBS) -o $@

%_noffmath_gcc.o : %.c 
    $(CC) $(CFLAGS) -c $< -o $@

et voici celui avec ce drapeau :

CC = gcc
CFLAGS = -O3 -ftree-vectorize  -march=native -Wall -std=gnu11 -fopenmp -fstrict-aliasing -ffast-math
LDFLAGS = -fPIE -fopenmp
LIBS =  -lm

SOURCES = inplace_array_mod_with_OpenMP.c
OBJECTS = $(SOURCES:.c=_gcc.o)
EXECUTABLE = inplace_array_mod_with_OpenMP_gcc

all: $(SOURCES) $(EXECUTABLE)

clean:
    rm -f $(OBJECTS) $(EXECUTABLE)

$(EXECUTABLE): $(OBJECTS)
    $(CC) $(LDFLAGS) $(OBJECTS) $(LIBS) -o $@

%_gcc.o : %.c 
    $(CC) $(CFLAGS) -c $< -o $@

Et voici les résultats de l'exécution de ces deux programmes

  • Sans -ffast-math
OMP_SCHEDULE=guided,1 OMP_NUM_THREADS=8 ./inplace_array_mod_with_OpenMP_noffmath_gcc 50000000
Array size: 50000000
Schedule from env guided,1
Number of threads = 8 from within C
Non-sequential calculation time: 1.12 seconds
Sequential calculation time: 0.95 seconds
Parallel calculation using OMP time: 0.17 seconds
Sequential calculation using OMP time: 0.15 seconds
  • Avec -ffast-math
OMP_SCHEDULE=guided,1 OMP_NUM_THREADS=8 ./inplace_array_mod_with_OpenMP_gcc 50000000
Array size: 50000000
Schedule from env guided,1
Number of threads = 8 from within C
Non-sequential calculation time: 0.27 seconds
Sequential calculation time: 0.28 seconds
Parallel calculation using OMP time: 0.05 seconds
Sequential calculation using OMP time: 0.06 seconds

Notez que l'on peut utiliser le fastmath dans le code Numba comme suit (la valeur par défaut est fastmath=False) :

@njit(nogil=True,fastmath=True)
def compute_inplace_with_numba(array):
    np.sqrt(array,array)
    np.sin(array,array)
    np.cos(array,array)

Quelques points à noter :

  • Le -ffast-math donne une amélioration majeure des performances (environ 300 % pour le code monothread et multithread), mais il peut générer des résultats erronés
  • Fastmath fonctionne également dans Numba, mais doit être évité pour les mêmes raisons qu'il doit être évité dans toute application qui recherche la précision
  • Le code séquentiel C monothread offre des performances similaires à celles du PDL et Numpy monothread
  • De manière quelque peu surprenante, le code séquentiel est environ 20 % plus rapide que le code non séquentiel lorsque les mathématiques correctes (non rapides) sont utilisées.
  • Sans surprise, le code multithread est plus rapide que le code monothread :)
  • Je n'arrive toujours pas à expliquer comment numbas offre une prime de performance de 50 % par rapport au code C de cette fonction plutôt simple.

titre : « La Quête de la Performance Partie III : C Force »

date: 2024-07-07

Dans les deux volets précédents de cette série, nous avons examiné les performances des opérations flottantes en Perl,
Python et R dans un exemple de jouet qui a calculé la fonction cos(sin(sqrt(x))), où x était un très grand tableau de 50 millions de nombres flottants double précision.
Les implémentations hybrides qui déléguaient la partie arithmétique intensive à C étaient parmi les implémentations les plus performantes. Dans cet article, nous allons faire une légère digression et examiner les performances d'une implémentation en code C pur de l'exemple de jouet.
Le code C fournira des informations supplémentaires sur l'importance de la localité mémoire pour les performances (par défaut, les éléments d'un tableau C sont stockés dans des adresses séquentielles en mémoire, et les API numériques telles que PDL ou l'interface numpy avec de tels conteneurs) par rapport aux conteneurs. ,
par ex. Tableaux Perl qui ne stockent pas leurs valeurs dans des adresses séquentielles en mémoire. Dernier point, mais non le moindre, les implémentations du code C nous permettront d'évaluer si les indicateurs liés aux opérations en virgule flottante pour le compilateur de bas niveau (dans ce cas gcc) peuvent affecter les performances.
Ce point mérite d'être souligné : le commun des mortels dépend entièrement du choix des indicateurs du compilateur lors du "piping" de son "installation" ou de la construction de son fichier Inline. Si l’on ne touche pas ces drapeaux, on ignorera parfaitement ce qu’ils pourraient manquer ou les pièges qu’ils pourraient éviter.
Le modeste makefile du fichier C permet de faire explicitement de telles évaluations de performances.

Le code C de notre exemple de jouet est répertorié dans son intégralité ci-dessous. Le code est plutôt explicite, je ne passerai donc pas de temps à expliquer autre que souligner qu'il contient quatre fonctions pour

  • Calcul non séquentiel de la fonction coûteuse : les trois opérations de pointage flottant s'effectuent dans une seule boucle à l'aide d'un seul thread
  • Calculs séquentiels de la fonction coûteuse : chacune des 3 évaluations de fonctions à virgule flottante se déroule dans une boucle séparée à l'aide d'un seul thread
  • Code OpenMP non séquentiel : version threadée du code non séquentiel
  • Code OpenMP séquentiel : thread du code séquentiel

Dans ce cas, on peut espérer que le compilateur soit suffisamment intelligent pour reconnaître que la racine carrée correspond à des opérations de virgule flottante compressées (vectorisées) dans l'assembly, de sorte qu'une fonction puisse être vectorisée à l'aide des instructions SIMD appropriées (notez que nous l'avons fait n'utilisez pas le programme simd pour les codes OpenMP).
Peut-être que l'accélération de la vectorisation pourrait compenser la perte de performances liée à l'accès répété aux mêmes emplacements mémoire (ou non).

#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <stdio.h>
#include <omp.h>

// simulates a large array of random numbers
double*  simulate_array(int num_of_elements,int seed);
// OMP environment functions
void _set_openmp_schedule_from_env();
void _set_num_threads_from_env();



// functions to modify C arrays 
void map_c_array(double* array, int len);
void map_c_array_sequential(double* array, int len);
void map_C_array_using_OMP(double* array, int len);
void map_C_array_sequential_using_OMP(double* array, int len);

int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("Usage: %s <array_size>\n", argv[0]);
        return 1;
    }

    int array_size = atoi(argv[1]);
    // printf the array size
    printf("Array size: %d\n", array_size);
    double *array = simulate_array(array_size, 1234);

    // Set OMP environment
    _set_openmp_schedule_from_env();
    _set_num_threads_from_env();

    // Perform calculations and collect timing data
    double start_time, end_time, elapsed_time;
    // Non-Sequential calculation
    start_time = omp_get_wtime();
    map_c_array(array, array_size);
    end_time = omp_get_wtime();
    elapsed_time = end_time - start_time;
    printf("Non-sequential calculation time: %f seconds\n", elapsed_time);
    free(array);

    // Sequential calculation
    array = simulate_array(array_size, 1234);
    start_time = omp_get_wtime();
    map_c_array_sequential(array, array_size);
    end_time = omp_get_wtime();
    elapsed_time = end_time - start_time;
    printf("Sequential calculation time: %f seconds\n", elapsed_time);
    free(array);

    array = simulate_array(array_size, 1234);
    // Parallel calculation using OMP
    start_time = omp_get_wtime();
    map_C_array_using_OMP(array, array_size);
    end_time = omp_get_wtime();
    elapsed_time = end_time - start_time;
    printf("Parallel calculation using OMP time: %f seconds\n", elapsed_time);
    free(array);

    // Sequential calculation using OMP
    array = simulate_array(array_size, 1234);
    start_time = omp_get_wtime();
    map_C_array_sequential_using_OMP(array, array_size);
    end_time = omp_get_wtime();
    elapsed_time = end_time - start_time;
    printf("Sequential calculation using OMP time: %f seconds\n", elapsed_time);

    free(array);
    return 0;
}



/*
*******************************************************************************
* OMP environment functions
*******************************************************************************
*/
void _set_openmp_schedule_from_env() {
  char *schedule_env = getenv("OMP_SCHEDULE");
  printf("Schedule from env %s\n", getenv("OMP_SCHEDULE"));
  if (schedule_env != NULL) {
    char *kind_str = strtok(schedule_env, ",");
    char *chunk_size_str = strtok(NULL, ",");

    omp_sched_t kind;
    if (strcmp(kind_str, "static") == 0) {
      kind = omp_sched_static;
    } else if (strcmp(kind_str, "dynamic") == 0) {
      kind = omp_sched_dynamic;
    } else if (strcmp(kind_str, "guided") == 0) {
      kind = omp_sched_guided;
    } else {
      kind = omp_sched_auto;
    }
    int chunk_size = atoi(chunk_size_str);
    omp_set_schedule(kind, chunk_size);
  }
}

void _set_num_threads_from_env() {
  char *num = getenv("OMP_NUM_THREADS");
  printf("Number of threads = %s from within C\n", num);
  omp_set_num_threads(atoi(num));
}
/*
*******************************************************************************
* Functions that modify C arrays whose address is passed from Perl in C
*******************************************************************************
*/

double*  simulate_array(int num_of_elements, int seed) {
  srand(seed); // Seed the random number generator
  double *array = (double *)malloc(num_of_elements * sizeof(double));
  for (int i = 0; i < num_of_elements; i++) {
    array[i] =
        (double)rand() / RAND_MAX; // Generate a random double between 0 and 1
  }
  return array;
}

void map_c_array(double *array, int len) {
  for (int i = 0; i < len; i++) {
    array[i] = cos(sin(sqrt(array[i])));
  }
}

void map_c_array_sequential(double* array, int len) {
  for (int i = 0; i < len; i++) {
    array[i] = sqrt(array[i]);
  }
  for (int i = 0; i < len; i++) {
    array[i] = sin(array[i]);
  }
  for (int i = 0; i < len; i++) {
    array[i] = cos(array[i]);
  }
}

void map_C_array_using_OMP(double* array, int len) {
#pragma omp parallel
  {
#pragma omp for schedule(runtime) nowait
    for (int i = 0; i < len; i++) {
      array[i] = cos(sin(sqrt(array[i])));
    }
  }
}

void map_C_array_sequential_using_OMP(double* array, int len) {
#pragma omp parallel
  {
#pragma omp for schedule(runtime) nowait
    for (int i = 0; i < len; i++) {
      array[i] = sqrt(array[i]);
    }
#pragma omp for schedule(runtime) nowait
    for (int i = 0; i < len; i++) {
      array[i] = sin(array[i]);
    }
#pragma omp for schedule(runtime) nowait
    for (int i = 0; i < len; i++) {
      array[i] = cos(array[i]);
    }
  }
}

Une question cruciale est de savoir si l'utilisation d'indicateurs de compilateur flottants rapides, une astuce qui échange la vitesse contre la précision du code, peut affecter les performances.
Voici le makefile sans ce drapeau du compilateur

CC = gcc
CFLAGS = -O3 -ftree-vectorize  -march=native  -Wall -std=gnu11 -fopenmp -fstrict-aliasing 
LDFLAGS = -fPIE -fopenmp
LIBS =  -lm

SOURCES = inplace_array_mod_with_OpenMP.c
OBJECTS = $(SOURCES:.c=_noffmath_gcc.o)
EXECUTABLE = inplace_array_mod_with_OpenMP_noffmath_gcc

all: $(SOURCES) $(EXECUTABLE)

clean:
    rm -f $(OBJECTS) $(EXECUTABLE)

$(EXECUTABLE): $(OBJECTS)
    $(CC) $(LDFLAGS) $(OBJECTS) $(LIBS) -o $@

%_noffmath_gcc.o : %.c 
    $(CC) $(CFLAGS) -c $< -o $@

et voici celui avec ce drapeau :

CC = gcc
CFLAGS = -O3 -ftree-vectorize  -march=native -Wall -std=gnu11 -fopenmp -fstrict-aliasing -ffast-math
LDFLAGS = -fPIE -fopenmp
LIBS =  -lm

SOURCES = inplace_array_mod_with_OpenMP.c
OBJECTS = $(SOURCES:.c=_gcc.o)
EXECUTABLE = inplace_array_mod_with_OpenMP_gcc

all: $(SOURCES) $(EXECUTABLE)

clean:
    rm -f $(OBJECTS) $(EXECUTABLE)

$(EXECUTABLE): $(OBJECTS)
    $(CC) $(LDFLAGS) $(OBJECTS) $(LIBS) -o $@

%_gcc.o : %.c 
    $(CC) $(CFLAGS) -c $< -o $@

Et voici les résultats de l'exécution de ces deux programmes

  • Sans -ffast-math
OMP_SCHEDULE=guided,1 OMP_NUM_THREADS=8 ./inplace_array_mod_with_OpenMP_noffmath_gcc 50000000
Array size: 50000000
Schedule from env guided,1
Number of threads = 8 from within C
Non-sequential calculation time: 1.12 seconds
Sequential calculation time: 0.95 seconds
Parallel calculation using OMP time: 0.17 seconds
Sequential calculation using OMP time: 0.15 seconds
  • Avec -ffast-math
OMP_SCHEDULE=guided,1 OMP_NUM_THREADS=8 ./inplace_array_mod_with_OpenMP_gcc 50000000
Array size: 50000000
Schedule from env guided,1
Number of threads = 8 from within C
Non-sequential calculation time: 0.27 seconds
Sequential calculation time: 0.28 seconds
Parallel calculation using OMP time: 0.05 seconds
Sequential calculation using OMP time: 0.06 seconds

Notez que l'on peut utiliser le fastmath dans le code Numba comme suit (la valeur par défaut est fastmath=False) :

@njit(nogil=True,fastmath=True)
def compute_inplace_with_numba(array):
    np.sqrt(array,array)
    np.sin(array,array)
    np.cos(array,array)

Quelques points à noter :

  • Le -ffast-math donne une amélioration majeure des performances (environ 300 % pour le code monothread et multithread), mais il peut générer des résultats erronés
  • Fastmath fonctionne également dans Numba, mais doit être évité pour les mêmes raisons qu'il doit être évité dans toute application qui recherche la précision
  • Le code séquentiel C monothread offre des performances similaires à celles du PDL et Numpy monothread
  • De manière quelque peu surprenante, le code séquentiel est environ 20 % plus rapide que le code non séquentiel lorsque les mathématiques correctes (non rapides) sont utilisées.
  • Sans surprise, le code multithread est plus rapide que le code monothread :)
  • Je n'arrive toujours pas à expliquer comment numbas offre une prime de performance de 50 % par rapport au code C de cette fonction plutôt simple.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn