2015-12-07 4 views
9

Я реализовал многопоточный метод Джордана-Гаусса для решения линейной системы, и я увидел, что работа на двух потоках заняла примерно на 15% меньше времени, чем работа на одной нити вместо идеальных 50%. Поэтому я написал простую программу, воспроизводящую это. Здесь я создаю матрицу 2000x2000 и даю 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. 

Это означает программу 2-нить работает 13% быстрее, чем однопоточных, даже хотя синхронизация между потоками отсутствует, и они не разделяют память. Я нашел этот ответ: https://stackoverflow.com/a/14812411/5647501 и подумал, что здесь могут быть некоторые проблемы с кешем процессора, поэтому я добавил прописку, но результат все равно остался прежним. Я изменил код следующим образом:

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-generic, процессор Intel Core i5 4200M (два физических ядра с многопоточным), но я также тестировал его на двух других машинах с тем же результатом.

EDIT: Если я заменяю a[j*row_length + k] -= 2.; с a[0] -= 2.;, я получаю ожидаемый Ускорить:

./threads_test1 
Duration: 1823.689481 msec. 
./threads_test2 
Duration: 949.745232 msec. 

EDIT 2: Теперь, когда я заменил его с a[k] -= 2.; я получаю следующее:

./threads_test1 
Duration: 1039.666979 msec. 
./threads_test2 
Duration: 1323.460080 msec. 

Этого я не могу получить вообще.

+0

Я голосую, чтобы закрыть этот вопрос как не по теме, потому что этот звук больше похож на вопрос для обзора кода. – Olaf

ответ

1

Вы говорите о 13% скорости, но какое время прошло на вашем исчислении, а не в остальной части программы.

Вы можете начать оценивать только время, пройденное методом расчета без времени управления потоками. Возможно, вы потеряете важную часть своего времени в управлении потоками. Это может объяснить небольшую скорость, которую вы получили.

В другой части 50% скорости с 2 потоками получить невозможно.

+0

Благодарим вас за ответ. Я пытался увеличить MATRIX_SIZE до 3000, и все же у меня есть 24 и 21 секунда. Я не думаю, что управление потоками здесь (создание 2 потоков и их объединение) займет так много времени – Matvey

7

Это классический вопрос, переключите i и j на циклы.

Вы выполняете итерацию по столбцам сначала, а во внутреннем цикле обрабатываете строки, это означает, что у вас гораздо больше промахов в кеше, чем необходимо.

Мои результаты с оригинальным кодом (первая версия без заполнения):

$ ./matrix_test1 
Duration: 4620.799763 msec. 
$ ./matrix_test2 
Duration: 2800.486895 msec. 

(лучше улучшение, чем у вас на самом деле)

После включения для петель для I и J:

$ ./matrix_test1 
Duration: 1450.037651 msec. 
$ ./matrix_test2 
Duration: 728.690853 msec. 

Здесь 2-кратное ускорение.

EDIT: В оригинале нет , что плохой, потому что индекс k по-прежнему проходит через строки, повторяющие столбцы, но все же намного лучше повторять строку во внешнем цикле. И когда i поднимается, вы обрабатываете все меньше и меньше предметов в самом внутреннем цикле, так что это все еще имеет значение.

EDIT2: (удалено блок-решение, потому что оно фактически создавало разные результаты), но все же должно быть возможно использовать блоки для повышения производительности кеша.

+0

Разве это может быть разница между нашими машинами? Потому что после переключения циклов для i и j я получаю следующее: ./ threads_test1 Продолжительность: 1048.321083 мс. ./threads_test2 Длительность: 1012,153498 мс. – Matvey

+0

ли вы на самом деле просто перешли петли, как это: для (J = 0, J axalis

+0

Попробуйте просто взять свой первый код в вопросе и просто переместите инструкцию «for (j ...» над оператором «for (i ...»), еще не обменивайте переменные. – axalis

Смежные вопросы