Heim >Backend-Entwicklung >C++ >Das Streben nach Leistung Teil III: C Force
In den beiden vorherigen Teilen dieser Serie haben wir uns mit der Leistung von Floating-Operationen in Perl befasst,
Python und R in einem Spielzeugbeispiel, das die Funktion cos(sin(sqrt(x))) berechnet hat, wobei x ein sehr großes Array von 50 Millionen Gleitkommazahlen mit doppelter Genauigkeit war.
Hybridimplementierungen, die den rechenintensiven Teil an C delegierten, gehörten zu den leistungsstärksten Implementierungen. In diesem Teil schweifen wir etwas ab und betrachten die Leistung einer reinen C-Code-Implementierung des Spielzeugbeispiels.
Der C-Code liefert weitere Einblicke in die Bedeutung der Speicherlokalität für die Leistung (standardmäßig werden Elemente in einem C-Array an sequentiellen Adressen im Speicher gespeichert, und numerische APIs wie PDL oder Numpy-Schnittstelle mit solchen Containern) im Vergleich zu Containern ,
z.B. Perl-Arrays, die ihre Werte nicht in sequentiellen Adressen im Speicher speichern. Zu guter Letzt ermöglichen uns die C-Code-Implementierungen zu beurteilen, ob Flags im Zusammenhang mit Gleitkommaoperationen für den Low-Level-Compiler (in diesem Fall gcc) die Leistung beeinträchtigen können.
Dieser Punkt ist hervorzuheben: Normalsterbliche sind vollständig von der Wahl der Compiler-Flags abhängig, wenn sie ihre „Installation“ über die Pipeline weiterleiten oder ihre Inline-Datei erstellen. Wenn man diese Flaggen nicht berührt, ist man sich glücklicherweise nicht bewusst, was sie möglicherweise übersehen oder welche Fallstricke sie vermeiden.
Das bescheidene C-Datei-Makefile ermöglicht es, solche Leistungsbewertungen explizit vorzunehmen.
Der C-Code für unser Spielzeugbeispiel ist unten vollständig aufgeführt. Der Code ist ziemlich selbsterklärend, daher werde ich keine Zeit mit Erklärungen verschwenden, außer darauf hinzuweisen, dass er vier Funktionen für
enthältIn diesem Fall kann man hoffen, dass der Compiler intelligent genug ist, um zu erkennen, dass die Quadratwurzel auf gepackte (vektorisierte) Gleitkommaoperationen in Assembler abgebildet wird, sodass eine Funktion mithilfe der entsprechenden SIMD-Anweisungen vektorisiert werden kann (beachten Sie, dass wir dies getan haben). Verwenden Sie nicht das simd-Programm für die OpenMP-Codes).
Möglicherweise kann die Beschleunigung durch die Vektorisierung den Leistungsverlust ausgleichen, der durch den wiederholten Zugriff auf dieselben Speicherorte entsteht (oder auch nicht).
#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]); } } }
Eine entscheidende Frage ist, ob die Verwendung schneller Floating-Compiler-Flags, ein Trick, der Geschwindigkeit gegen Genauigkeit des Codes tauscht, die Leistung beeinträchtigen kann.
Hier ist das Makefile ohne dieses Compiler-Flag
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 $@
und hier ist das mit dieser Flagge:
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 $@
Und hier sind die Ergebnisse der Ausführung dieser beiden Programme
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
Beachten Sie, dass man Fastmath im Numba-Code wie folgt verwenden kann (die Standardeinstellung ist 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)
Ein paar Punkte, die es zu beachten gilt:
Titel: „The Quest for Performance Part III: C Force“
In den beiden vorherigen Teilen dieser Serie haben wir uns mit der Leistung von Floating-Operationen in Perl befasst,
Python und R in einem Spielzeugbeispiel, das die Funktion cos(sin(sqrt(x))) berechnet hat, wobei x ein sehr großes Array von 50 Millionen Gleitkommazahlen mit doppelter Genauigkeit war.
Hybridimplementierungen, die den rechenintensiven Teil an C delegierten, gehörten zu den leistungsstärksten Implementierungen. In diesem Teil schweifen wir etwas ab und betrachten die Leistung einer reinen C-Code-Implementierung des Spielzeugbeispiels.
Der C-Code liefert weitere Einblicke in die Bedeutung der Speicherlokalität für die Leistung (standardmäßig werden Elemente in einem C-Array an sequentiellen Adressen im Speicher gespeichert, und numerische APIs wie PDL oder Numpy-Schnittstelle mit solchen Containern) im Vergleich zu Containern ,
z.B. Perl-Arrays, die ihre Werte nicht in sequentiellen Adressen im Speicher speichern. Zu guter Letzt ermöglichen uns die C-Code-Implementierungen zu beurteilen, ob Flags im Zusammenhang mit Gleitkommaoperationen für den Low-Level-Compiler (in diesem Fall gcc) die Leistung beeinträchtigen können.
Dieser Punkt ist hervorzuheben: Normalsterbliche sind vollständig von der Wahl der Compiler-Flags abhängig, wenn sie ihre „Installation“ über die Pipeline weiterleiten oder ihre Inline-Datei erstellen. Wenn man diese Flaggen nicht berührt, ist man sich glücklicherweise nicht bewusst, was sie möglicherweise übersehen oder welche Fallstricke sie vermeiden.
Das bescheidene C-Datei-Makefile ermöglicht es, solche Leistungsbewertungen explizit durchzuführen.
Der C-Code für unser Spielzeugbeispiel ist unten vollständig aufgeführt. Der Code ist ziemlich selbsterklärend, daher werde ich keine Zeit mit Erklärungen verschwenden, außer darauf hinzuweisen, dass er vier Funktionen für
enthältIn diesem Fall kann man hoffen, dass der Compiler intelligent genug ist, um zu erkennen, dass die Quadratwurzel auf gepackte (vektorisierte) Gleitkommaoperationen in Assembler abgebildet wird, sodass eine Funktion mithilfe der entsprechenden SIMD-Anweisungen vektorisiert werden kann (beachten Sie, dass wir dies getan haben). Verwenden Sie nicht das simd-Programm für die OpenMP-Codes).
Möglicherweise kann die Beschleunigung durch die Vektorisierung den Leistungsverlust ausgleichen, der durch den wiederholten Zugriff auf dieselben Speicherorte entsteht (oder auch nicht).
#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]); } } }
Eine entscheidende Frage ist, ob die Verwendung schneller Floating-Compiler-Flags, ein Trick, der Geschwindigkeit gegen Genauigkeit des Codes tauscht, die Leistung beeinträchtigen kann.
Hier ist das Makefile ohne dieses Compiler-Flag
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 $@
und hier ist das mit dieser Flagge:
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 $@
Und hier sind die Ergebnisse der Ausführung dieser beiden Programme
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
Beachten Sie, dass man Fastmath im Numba-Code wie folgt verwenden kann (der Standardwert ist 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)
Ein paar Punkte, die es zu beachten gilt:
Das obige ist der detaillierte Inhalt vonDas Streben nach Leistung Teil III: C Force. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!