Небольшое увеличение производительности при использовании нескольких потоков
Я реализовал многопоточный метод Джордана-Гаусса для решения линейной системы и увидел, что работа на двух потоках занимает всего лишь примерно на 15% меньше времени, чем работа на одном потоке, вместо идеальных 50%. Поэтому я написал простую программу, воспроизводящую это. Здесь я создаю матрицу 2000x 2000 и даю 2000/THREADS_NUM строк каждому потоку, чтобы сделать некоторые вычисления с ними.
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <time.h>
#ifndef THREADS_NUM
#define THREADS_NUM 1
#endif
#define MATRIX_SIZE 2000
typedef struct {
double *a;
int row_length;
int rows_number;
} TWorkerParams;
void *worker_thread(void *params_v)
{
TWorkerParams *params = (TWorkerParams *)params_v;
int row_length = params->row_length;
int i, j, k;
int rows_number = params->rows_number;
double *a = params->a;
for(i = 0; i < row_length; ++i) // row_length is always the same
{
for(j = 0; j < rows_number; ++j) // rows_number is inverse proportional
// to the number of threads
{
for(k = i; k < row_length; ++k) // row_length is always the same
{
a[j*row_length + k] -= 2.;
}
}
}
return NULL;
}
int main(int argc, char *argv[])
{
// The matrix is of size NxN
double *a =
(double *)malloc(MATRIX_SIZE * MATRIX_SIZE * sizeof(double));
TWorkerParams *params =
(TWorkerParams *)malloc(THREADS_NUM * sizeof(TWorkerParams));
pthread_t *workers = (pthread_t *)malloc(THREADS_NUM * sizeof(pthread_t));
struct timespec start_time, end_time;
int rows_per_worker = MATRIX_SIZE / THREADS_NUM;
int i;
if(!a || !params || !workers)
{
fprintf(stderr, "Error allocating memory\n");
return 1;
}
for(i = 0; i < MATRIX_SIZE*MATRIX_SIZE; ++i)
a[i] = 4. * i; // just an example matrix
// Initializtion of matrix is done, now initialize threads' params
for(i = 0; i < THREADS_NUM; ++i)
{
params[i].a = a + i * rows_per_worker * MATRIX_SIZE;
params[i].row_length = MATRIX_SIZE;
params[i].rows_number = rows_per_worker;
}
// Get start time
clock_gettime(CLOCK_MONOTONIC, &start_time);
// Create threads
for(i = 0; i < THREADS_NUM; ++i)
{
if(pthread_create(workers + i, NULL, worker_thread, params + i))
{
fprintf(stderr, "Error creating thread\n");
return 1;
}
}
// Join threads
for(i = 0; i < THREADS_NUM; ++i)
{
if(pthread_join(workers[i], NULL))
{
fprintf(stderr, "Error creating thread\n");
return 1;
}
}
clock_gettime(CLOCK_MONOTONIC, &end_time);
printf("Duration: %lf msec.\n", (end_time.tv_sec - start_time.tv_sec)*1e3 +
(end_time.tv_nsec - start_time.tv_nsec)*1e-6);
return 0;
}
Вот как я это скомпилирую:
gcc threads_test.c -o threads_test1 -lrt -pthread -DTHREADS_NUM=1 -Wall -Werror -Ofast
gcc threads_test.c -o threads_test2 -lrt -pthread -DTHREADS_NUM=2 -Wall -Werror -Ofast
Теперь, когда я бегу, я получаю:
./threads_test1
Duration: 3695.359552 msec.
./threads_test2
Duration: 3211.236612 msec.
Это означает, что двухпоточная программа работает на 13% быстрее, чем однопоточная, хотя синхронизация между потоками отсутствует, и они не разделяют память. Я нашел этот ответ: /questions/40500744/mnogopotochnyij-gemm-medlennee-odnopotochnogo/40500753#40500753 и подумал, что здесь могут быть некоторые проблемы с кэшем процессора, поэтому я добавил заполнение, но результат все равно остался прежним. Я изменил свой код следующим образом:
typedef struct {
double *a;
int row_length;
int rows_number;
volatile char padding[64 - 2*sizeof(int)-sizeof(double)];
} TWorkerParams;
#define VAR_SIZE (sizeof(int)*5 + sizeof(double)*2)
#define MEM_SIZE ((VAR_SIZE / 64 + 1) * 64 )
void *worker_thread(void *params_v)
{
TWorkerParams *params = (TWorkerParams *)params_v;
volatile char memory[MEM_SIZE];
int *row_length = (int *)(memory + 0);
int *i = (int *)(memory + sizeof(int)*1);
int *j = (int *)(memory + sizeof(int)*2);
int *k = (int *)(memory + sizeof(int)*3);
int *rows_number = (int *)(memory + sizeof(int)*4);
double **a = (double **)(memory + sizeof(int)*5);
*row_length = params->row_length;
*rows_number = params->rows_number;
*a = params->a;
for(*i = 0; *i < *row_length; ++*i) // row_length is always the same
{
for(*j = 0; *j < *rows_number; ++*j) // rows_number is inverse proportional
// to the number of threads
{
for(*k = 0; *k < *row_length; ++*k) // row_length is always the same
{
(*a + *j * *row_length)[*k] -= 2. * *k;
}
}
}
return NULL;
}
Итак, мой вопрос: почему я получаю ускорение только на 15% вместо 50% при использовании двух потоков здесь? Любая помощь или предложение будут оценены. Я использую 64-разрядную версию Ubuntu Linux, ядро 3.19.0-39, процессор Intel Core i5 4200M (два физических ядра с многопоточностью), но я также протестировал его на двух других машинах с тем же результатом.
РЕДАКТИРОВАТЬ: если я заменю a[j*row_length + k] -= 2.;
с a[0] -= 2.;
Я получаю ожидаемое ускорение:
./threads_test1
Duration: 1823.689481 msec.
./threads_test2
Duration: 949.745232 msec.
РЕДАКТИРОВАТЬ 2: Теперь, когда я заменил его a[k] -= 2.;
Я получаю следующее:
./threads_test1
Duration: 1039.666979 msec.
./threads_test2
Duration: 1323.460080 msec.
Этот я не могу получить вообще.
2 ответа
Это классическая проблема, переключите i и j для циклов.
Сначала вы перебираете столбцы, а во внутреннем цикле обрабатываете строки, это означает, что у вас гораздо больше пропусков кэша, чем необходимо.
Мои результаты с оригинальным кодом (первая версия без отступов):
$ ./matrix_test1
Duration: 4620.799763 msec.
$ ./matrix_test2
Duration: 2800.486895 msec.
(лучшее улучшение, чем у тебя на самом деле)
После переключения циклов for для i и j:
$ ./matrix_test1
Duration: 1450.037651 msec.
$ ./matrix_test2
Duration: 728.690853 msec.
Здесь ускорение в 2 раза.
РЕДАКТИРОВАТЬ: На самом деле оригинал не так уж и плохо, потому что индекс k по-прежнему проходит через столбцы итерации строк, но гораздо лучше итерацию строки во внешнем цикле. И когда i повышается, вы обрабатываете все меньше и меньше элементов в самом внутреннем цикле, так что это все еще имеет значение.
EDIT2: (удалено решение для блоков, потому что оно на самом деле давало разные результаты) - но все же должно быть возможно использовать блоки для повышения производительности кэша.
Говорите ли вы о 13 % ускорения, но сколько времени прошло с вашего исчисления, а не с остальной частью программы?
Вы можете начать оценивать только время, прошедшее с помощью метода calcul, без времени управления потоками. Возможно, вы потеряете важную часть своего времени в управлении потоками. Это может объяснить небольшую скорость, которую вы получили.
С другой стороны, 50% ускорения с помощью двух потоков совершенно невозможно получить.