почему эта параллельная функция для вычисления самой длинной общей подпоследовательности медленнее, чем последовательная?

Параллельное вычисление LCS следует схеме волнового фронта.
Вот параллельная функция, которая медленнее, чем последовательная реализация. (Я думаю, что количество диагоналей (параллельных) и количество рядов (последовательных) как-то связано с этим)

void parallelLCS(char * sequence_a, char * sequence_b, size_t size_a, size_t size_b) {
double start, end;

int ** dp_table = new int*[size_a + 1];

for (int i = 0; i <= size_a; i++)
dp_table[i] = new int[size_b + 1];

for (int i = 1; i <= size_a; i++)
dp_table[i][0] = 0;
for (int j = 0; j <= size_b; j++)
dp_table[0][j] = 0;

int p_threads = 2;
int diagonals = size_a + size_b;

start = omp_get_wtime();
#pragma omp parallel num_threads(p_threads) default(none) firstprivate(p_threads,size_a,size_b,sequence_a,sequence_b) shared(dp_table,diagonals)
{
for (int curr_diagonal = 1; curr_diagonal <= (diagonals - 1);) {
int j = omp_get_thread_num() + 1;   //column index
int i = curr_diagonal - j + 1;      //row index
for (; j <= curr_diagonal; j += p_threads, i = i - p_threads) {
if (i <= size_a && j <= size_b) {
if (sequence_a[i] == sequence_b[j]) {
dp_table[i][j] = dp_table[i - 1][j - 1] + 1;
} else if (dp_table[i - 1][j] >= dp_table[i][j - 1]) {
dp_table[i][j] = dp_table[i - 1][j];
} else {
dp_table[i][j] = dp_table[i][j - 1];
}
}
}
curr_diagonal++;
#pragma omp barrier
}
}
end = omp_get_wtime();

printf("\nParallel - Final answer: %d\n", dp_table[size_a][size_b]);
printf("Time: %f\n", end - start);

//Delete dp_table
for (int i = 0; i <= size_a; i++)
delete [] dp_table[i];
delete [] dp_table;
}

и вот последовательная функция

void serialLCS(char * sequence_a, char * sequence_b, size_t size_a, size_t size_b) {
double start, end;
int ** dp_table = new int*[size_a + 1];
for (int i = 0; i <= size_a; i++)
dp_table[i] = new int[size_b + 1];

for (int i = 1; i <= size_a; i++)
dp_table[i][0] = 0;
for (int j = 0; j <= size_b; j++)
dp_table[0][j] = 0;

start = omp_get_wtime();
for (int i = 1; i <= size_a; i++) {
for (int j = 1; j <= size_b; j++) {
if (sequence_a[i] == sequence_b[j]) {
dp_table[i][j] = dp_table[i - 1][j - 1] + 1;
} else if (dp_table[i - 1][j] >= dp_table[i][j - 1]) {
dp_table[i][j] = dp_table[i - 1][j];
} else {
dp_table[i][j] = dp_table[i][j - 1];
}
}
}
end = omp_get_wtime();
printf("\nSerial - Final answer: %d\n", dp_table[size_a][size_b]);
printf("Time: %f\n", end - start);

//Delete dp_table
for (int i = 0; i <= size_a; i++)
delete [] dp_table[i];
delete [] dp_table;
}

…думал, что я добавлю тестовую функцию

#include <cstdlib>
#include <stdio.h>

#include <omp.h>

void serialLCS(char * sequence_a, char * sequence_b, size_t size_a, size_t size_b);
void parallelLCS(char * sequence_a, char * sequence_b, size_t size_a, size_t size_b);

int main() {

size_t size_a;
size_t size_b;

printf("Enter size of sequence A: ");
scanf("%zd",&size_a);
printf("Enter size of sequence B: ");
scanf("%zd",&size_b);

//keep larger sequence in sequence_a
if (size_b > size_a) size_a ^= size_b ^= size_a ^= size_b;

char * sequence_a = new char[size_a + 1];
char * sequence_b = new char[size_b + 1];
sequence_a[0] = sequence_b[0] = '0';

const size_t alphabet_size = 12;
char A[alphabet_size] = {'A', 'T', 'G', 'C', 'Q', 'W', 'E', 'R', 'Y', 'U', 'I', 'O'};
char AA[alphabet_size] = {'T', 'C', 'A', 'G', 'R', 'E', 'W', 'Q', 'O', 'I', 'U', 'Y'};

for (size_t i = 1; i < size_a; i++) {
sequence_a[i] = A[rand() % alphabet_size];
}
for (size_t i = 1; i < size_b; i++) {
sequence_b[i] = AA[rand() % alphabet_size];
}

serialLCS(sequence_a, sequence_b, size_a, size_b);
parallelLCS(sequence_a, sequence_b, size_a, size_b);

delete [] sequence_a;
delete [] sequence_b;

return 0;
}

2

Решение

Проблема не в OpenMP, а в способе доступа к данным в параллельной реализации. Даже если вы запускаете параллельную версию только с одним потоком, она все равно примерно вдвое медленнее.

Что ж, добро пожаловать в мир не-кеширующих структур данных. Из-за зависимости от диагонали вы проходите матрицу по диагонали, но сохраняете ее обычным способом. Шаблон доступа к данным тогда сильно нелинейный и, следовательно, очень недружественный к кешу. Обратите внимание на количество пропусков кэша L1 и L2 при запуске кода в однопоточном режиме в старой 16-ядерной системе Xeon X7350:

Отсутствует кеш L1 и L2

Зеленая часть временной шкалы процесса представляет собой последовательную часть вашего кода. Оранжевая часть (неактивная из-за однопоточного исполнения) OpenMP parallel область, край. Вы можете ясно видеть, что последовательный код очень дружествен к кешу — не только количество пропусков кеша L2 является относительно низким, но также и объем кеша L1. Но в параллельном разделе кода, из-за очень большого шага при обходе матрицы по диагонали, кэш постоянно уничтожается, и количество промахов невероятно велико.

С двумя нитями все становится еще хуже. Элементы из двух соседних диагоналей, которые принадлежат одной строке матрицы, скорее всего, попадут в одну и ту же строку кэша. Но одна из диагоналей обрабатывается одной нитью, а другая — другой. Следовательно, ваш код сталкивается с огромным количеством ложного обмена. Не говоря уже о проблемах с NUMA в современных многопроцессорных системах AMD64 или (после) Nehalem.

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

8

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]