이 시리즈의 이전 두 기사에서는 Perl의 부동 연산 성능을 고려했습니다.
cos(sin(sqrt(x))) 함수를 계산하는 장난감 예제의 Python 및 R. 여기서 x는 50M 배정밀도 부동 숫자로 구성된 매우 큰 배열입니다.
산술 집약적인 부분을 C에 위임한 하이브리드 구현은 가장 성능이 뛰어난 구현 중 하나였습니다. 이번 회에서는 약간 다른 내용으로 장난감 예제의 순수 C 코드 구현 성능을 살펴보겠습니다.
C 코드는 성능을 위한 메모리 지역성의 중요성에 대한 추가 통찰력을 제공합니다(기본적으로 C 배열의 요소는 메모리의 순차 주소와 PDL 또는 해당 컨테이너와의 numpy 인터페이스와 같은 숫자 API에 저장됨). ,
예를 들어 메모리의 순차 주소에 값을 저장하지 않는 Perl 배열입니다. 마지막으로 C 코드 구현을 통해 저수준 컴파일러(이 경우 gcc)의 부동 소수점 연산과 관련된 플래그가 성능에 영향을 미칠 수 있는지 여부를 평가할 수 있습니다.
이 점은 강조할 가치가 있습니다. 일반 사용자는 "설치"를 "파이핑"하거나 인라인 파일을 빌드할 때 컴파일러 플래그 선택에 전적으로 의존합니다. 이 플래그를 건드리지 않으면 플래그가 무엇을 놓쳤는지, 피하고 있는지 전혀 알 수 없게 됩니다.
간단한 C 파일 makefile을 사용하면 이러한 성능 평가를 명시적으로 수행할 수 있습니다.
장난감 예제의 C 코드 전체가 아래에 나열되어 있습니다. 코드는 설명이 필요 없기 때문에
에 대한 네 가지 기능이 포함되어 있다는 점을 지적하는 것 외에는 설명하는 데 시간을 소비하지 않을 것입니다.이 경우 컴파일러가 제곱근이 어셈블리의 압축된(벡터화된) 부동 소수점 연산에 매핑된다는 것을 인식할 만큼 똑똑하여 하나의 함수가 적절한 SIMD 명령어를 사용하여 벡터화될 수 있기를 바랄 수 있습니다. OpenMP 코드에는 simd 프로그램을 사용하지 마십시오.
아마도 벡터화로 인한 속도 향상은 동일한 메모리 위치에 반복적으로 액세스하거나 액세스하지 않음으로 인한 성능 손실을 상쇄할 수 있습니다.
#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]); } } }
중요한 질문은 코드의 정확성을 위해 속도를 희생하는 빠른 부동 컴파일러 플래그를 사용하는 것이 성능에 영향을 미칠 수 있는지 여부입니다.
다음은 이 컴파일러 플래그가 없는 makefile입니다
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 $@
이 플래그가 있는 항목은 다음과 같습니다.
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 $@
그리고 이 두 프로그램을 실행한 결과는 다음과 같습니다
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
다음과 같이 Numba 코드에서 fastmath를 사용할 수 있습니다(기본값은 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)
주목할 만한 몇 가지 사항:
제목: " 퍼포먼스를 향한 탐구 3부 : C Force "
이 시리즈의 이전 두 기사에서는 Perl의 부동 연산 성능을 고려했습니다.
cos(sin(sqrt(x))) 함수를 계산하는 장난감 예제의 Python 및 R. 여기서 x는 50M 배정밀도 부동 숫자로 구성된 매우 큰 배열입니다.
산술 집약적인 부분을 C에 위임한 하이브리드 구현은 가장 성능이 뛰어난 구현 중 하나였습니다. 이번 회에서는 약간 다른 내용으로 장난감 예제의 순수 C 코드 구현 성능을 살펴보겠습니다.
C 코드는 성능을 위한 메모리 지역성의 중요성에 대한 추가 통찰력을 제공합니다(기본적으로 C 배열의 요소는 메모리의 순차 주소와 PDL 또는 해당 컨테이너와의 numpy 인터페이스와 같은 숫자 API에 저장됨). ,
예를 들어 메모리의 순차 주소에 값을 저장하지 않는 Perl 배열입니다. 마지막으로 C 코드 구현을 통해 저수준 컴파일러(이 경우 gcc)의 부동 소수점 연산과 관련된 플래그가 성능에 영향을 미칠 수 있는지 여부를 평가할 수 있습니다.
이 점은 강조할 가치가 있습니다. 일반 사용자는 "설치"를 "파이핑"하거나 인라인 파일을 빌드할 때 컴파일러 플래그 선택에 전적으로 의존합니다. 이 플래그를 건드리지 않으면 플래그가 무엇을 놓쳤는지, 피하고 있는지 전혀 알 수 없게 됩니다.
간단한 C 파일 makefile을 사용하면 이러한 성능 평가를 명시적으로 수행할 수 있습니다.
장난감 예제의 C 코드 전체가 아래에 나열되어 있습니다. 코드는 설명이 필요 없기 때문에
에 대한 네 가지 기능이 포함되어 있다는 점을 지적하는 것 외에는 설명하는 데 시간을 할애하지 않을 것입니다.이 경우 컴파일러가 제곱근이 어셈블리의 압축된(벡터화된) 부동 소수점 연산에 매핑된다는 것을 인식할 만큼 똑똑하여 하나의 함수가 적절한 SIMD 명령어를 사용하여 벡터화될 수 있기를 바랄 수 있습니다. OpenMP 코드에는 simd 프로그램을 사용하지 마십시오.
아마도 벡터화로 인한 속도 향상은 동일한 메모리 위치에 반복적으로 액세스하거나 액세스하지 않음으로 인한 성능 손실을 상쇄할 수 있습니다.
#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]); } } }
중요한 질문은 코드의 정확성을 위해 속도를 희생하는 빠른 부동 컴파일러 플래그를 사용하는 것이 성능에 영향을 미칠 수 있는지 여부입니다.
다음은 이 컴파일러 플래그가 없는 makefile입니다
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 $@
이 플래그가 있는 항목은 다음과 같습니다.
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 $@
그리고 이 두 프로그램을 실행한 결과는 다음과 같습니다
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
다음과 같이 Numba 코드에서 fastmath를 사용할 수 있습니다(기본값은 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)
주목할 만한 몇 가지 사항:
위 내용은 성능 탐구 3부: C Force의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!