Maison >développement back-end >C++ >La Quête de la Performance Partie 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
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
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
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 :
titre : « La Quête de la Performance Partie 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
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
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
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 :
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!