Параллелизм с использованием TBB — что должно быть в нашем контрольном списке?

Лишь совсем недавно перспективы параллельного программирования привлекли мое внимание.
С тех пор я использовал множество библиотек параллельного программирования. Возможно мой
Первой остановкой стали Intel Thread Building Blocks (TBB). Но узкими местами часто становились ошибки из-за таких факторов, как округления и непредсказуемое поведение этих программ в разных процессорных архитектурах. Ниже приведен фрагмент кода, который вычисляет коэффициент корреляции Пирсона для двух наборов значений. Он использует очень простые параллельные шаблоны TBB — * parallel_for * и * parallel_reduce *:

    // A programme to calculate Pearsons Correlation coefficient

#include <math.h>
#include <stdlib.h>
#include <iostream>
#include <tbb/task_scheduler_init.h>
#include <tbb/parallel_for.h>
#include <tbb/parallel_reduce.h>
#include <tbb/blocked_range.h>
#include <tbb/tick_count.h>using namespace std;
using namespace tbb;
const size_t n=100000;
double global=0;

namespace s //Namesapce for serial part
{
double *a,*b;
int j;
double mean_a,mean_b,sd_a=0,sd_b=0,pcc=0;
double sum_a,sum_b,i;
}

namespace p //Namespace for parallel part
{
double *a,*b;
double mean_a,mean_b,pcc;
double sum_a,sum_b,i;
double sd_a,sd_b;
}class serials
{
public:
void computemean_serial()
{
using namespace s;
sum_a=0,sum_b=0,i=0;
a=(double*) malloc(n*sizeof(double));
b=(double*) malloc(n*sizeof(double));
for(j=0;j<n;j++,i++)
{
a[j]=sin(i);
b[j]=cos(i);

sum_a=sum_a+a[j];
sum_b=sum_b+b[j];
}
mean_a=sum_a/n;
mean_b=sum_b/n;
cout<<"\nMean of a :"<<mean_a;
cout<<"\nMean of b :"<<mean_b;
}
void computesd_serial()
{
using namespace s;
for(j=0;j<n;j++)
{sd_a=sd_a+pow((a[j]-mean_a),2);
sd_b=sd_b+pow((b[j]-mean_b),2);
}
sd_a=sd_a/n;
sd_a=sqrt(sd_a);
sd_b=sd_b/n;
sd_b=sqrt(sd_b);
cout<<"\nStandard deviation of a :"<<sd_a;
cout<<"\nStandard deviation of b :"<<sd_b;
}
void pearson_correlation_coefficient_serial()
{
using namespace s;
pcc=0;
for(j=0;j<n;j++)
{
pcc+=(a[j]-mean_a)*(b[j]-mean_b);
}
pcc=pcc/(n*sd_a*sd_b);
cout<<"\nPearson Correlation Coefficient: "<<pcc;
}

};class parallel
{
public:

class compute_mean
{

double *store1,*store2;
public:

double mean_a,mean_b;

void operator()( const blocked_range<size_t>& r)
{
double *a= store1;
double *b= store2;

for(size_t i =r.begin();i!=r.end(); ++i)
{
mean_a+=a[i];
mean_b+=b[i];
}
}
compute_mean( compute_mean& x, split) : store1(x.store1),store2(x.store2),mean_a(0),mean_b(0){}

void join(const compute_mean& y) {mean_a+=y.mean_a;mean_b+=y.mean_b;}
compute_mean(double* a,double* b): store1(a),store2(b),mean_a(0),mean_b(0){}
};

class read_array
{
double *const a,*const b;

public:

read_array(double* vec1, double* vec2) : a(vec1),b(vec2){}  // constructor copies the arguments into local store
void operator() (const blocked_range<size_t> &r) const {              // opration to be used in parallel_for

for(size_t k = r.begin(); k!=r.end(); k++,global++)
{
a[k]=sin(global);
b[k]=cos(global);
}

}};

void computemean_parallel()
{
using namespace p;
i=0;
a=(double*) malloc(n*sizeof(double));
b=(double*) malloc(n*sizeof(double));

parallel_for(blocked_range<size_t>(0,n,5000),read_array(a,b));
compute_mean sf(a,b);
parallel_reduce(blocked_range<size_t>(0,n,5000),sf);
mean_a=sf.mean_a/n;
mean_b=sf.mean_b/n;
cout<<"\nMean of a :"<<mean_a;
cout<<"\nMean of b :"<<mean_b;
}

class compute_sd
{
double *store1,*store2;
double store3,store4;
public:
double sd_a,sd_b,dif_a,dif_b,temp_pcc;
void operator()( const blocked_range<size_t>& r)
{
double *a= store1;
double *b= store2;
double mean_a=store3;
double mean_b=store4;
for(size_t i =r.begin();i!=r.end(); ++i)
{
dif_a=a[i]-mean_a;
dif_b=b[i]-mean_b;
temp_pcc+=dif_a*dif_b;
sd_a+=pow(dif_a,2);
sd_b+=pow(dif_b,2);
}}
compute_sd( compute_sd& x, split) : store1(x.store1),store2(x.store2),store3(p::mean_a),store4(p::mean_b),sd_a(0),sd_b(0),temp_pcc(0){}
void join(const compute_sd& y) {sd_a+=y.sd_a;sd_b+=y.sd_b;}
compute_sd(double* a,double* b,double mean_a,double mean_b): store1(a),store2(b),store3(mean_a),store4(mean_b),sd_a(0),sd_b(0),temp_pcc(0){}
};void computesd_and_pearson_correlation_coefficient_parallel()
{
using namespace p;
compute_sd obj2(a,b,mean_a,mean_b);
parallel_reduce(blocked_range<size_t>(0,n,5000),obj2);
sd_a=obj2.sd_a;
sd_b=obj2.sd_b;
sd_a=sd_a/n;
sd_a=sqrt(sd_a);
sd_b=sd_b/n;
sd_b=sqrt(sd_b);
cout<<"\nStandard deviation of a :"<<sd_a;
cout<<"\nStandard deviation of b :"<<sd_b;
pcc=obj2.temp_pcc;
pcc=pcc/(n*sd_a*sd_b);
cout<<"\nPearson Correlation Coefficient: "<<pcc;
}
};

main()
{
serials obj_s;
parallel obj_p;
cout<<"\nSerial Part";
cout<<"\n-----------";
tick_count start_s=tick_count::now();
obj_s.computemean_serial();
obj_s.computesd_serial();
obj_s.pearson_correlation_coefficient_serial();
tick_count end_s=tick_count::now();
cout<<"\n";
task_scheduler_init init;
cout<<"\nParallel Part";
cout<<"\n-------------";
tick_count start_p=tick_count::now();
obj_p.computemean_parallel();
obj_p.computesd_and_pearson_correlation_coefficient_parallel();
tick_count end_p=tick_count::now();
cout<<"\n";
cout<<"\nTime Estimates";
cout<<"\n--------------";
cout<<"\nSerial Time :"<<(end_s-start_s).seconds()<<" Seconds";
cout<<"\nParallel time :"<<(end_p-start_p).seconds()<<" Seconds\n";

}

Что ж ! он работал нормально на Windows Machine с Core i5 внутри. Это дало мне абсолютно одинаковые значения для каждого параметра в выводе с параллельным кодовым коллектором быстрее, чем последовательный код. Вот мой выход:

Операционные системы : Windows 7 Ultimate, 64-разрядная версия процессор : Core i5

Serial Part
-----------
Mean of a :1.81203e-05
Mean of b :1.0324e-05
Standard deviation of a :0.707107
Standard deviation of b :0.707107
Pearson Correlation Coefficient: 3.65091e-07

Parallel Part
-------------
Mean of a :1.81203e-05
Mean of b :1.0324e-05
Standard deviation of a :0.707107
Standard deviation of b :0.707107
Pearson Correlation Coefficient: 3.65091e-07

Time Estimates
--------------
Serial Time : 0.0204829 Seconds
Parallel Time : 0.00939971 Seconds

Так что насчет других машин? Если я скажу, что все будет хорошо, то, по крайней мере, некоторые из моих друзей скажут: «Подожди, приятель! Что-то подозрительно». Были небольшие различия в ответах (между ответами, создаваемыми параллельным и последовательным кодом) на разных машинах, хотя параллельный код всегда был быстрее, чем последовательный. Так, что сделало эти различия? Мы пришли к выводу, что для этого ненормального поведения были ошибки округления, которые происходят за счет чрезмерного параллелизма и различий в архитектуре процессора.

Это приводит к моим вопросам:

  • Какие меры предосторожности мы должны предпринять при использовании параллельного
    обработка библиотек в наших кодах, чтобы использовать преимущества многоядерности
    Процессоры?
  • В каких ситуациях мы не должны использовать параллельный подход?
    хотя есть наличие нескольких процессоров?
  • Что лучше всего сделать, чтобы избежать ошибок округления? (Позвольте мне
    указать, что я не говорю о соблюдении мьютексов и барьеров, которые
    может когда-нибудь поставить ограничение на степень параллелизма, но о
    простые советы по программированию, которые иногда могут быть полезны)

Я очень рад видеть ваши предложения по этим вопросам. Пожалуйста, будьте свободны ответить
часть, которая лучше всего подходит вам, если у вас есть ограничения по времени.

Изменить — я включил больше результатов здесь

Операционные системы : Linux Ubuntu 64 бит процессор : Core i5

    Serial Part
-----------
Mean of a :1.81203e-05
Mean of b :1.0324e-05
Standard deviation of a :0.707107
Standard deviation of b :0.707107
Pearson Correlation Coefficient: 3.65091e-07

Parallel Part
-------------
Mean of a :-0.000233041
Mean of b :0.00414375
Standard deviation of a :2.58428
Standard deviation of b :54.6333
Pearson Correlation Coefficient: -0.000538456

Time Estimates
--------------
Serial Time :0.0161237 Seconds
Parallel Time :0.0103125 Seconds

Операционные системы : Linux Fedora 64 бит процессор : core i3

Serial Part
-----------
Mean of a :1.81203e-05
Mean of b :1.0324e-05
Standard deviation of a :0.707107
Standard deviation of b :0.707107
Pearson Correlation Coefficient: 3.65091e-07

Parallel Part
-------------
Mean of a :-0.00197118
Mean of b :0.00124329
Standard deviation of a :0.707783
Standard deviation of b :0.703951
Pearson Correlation Coefficient: -0.129055

Time Estimates
--------------
Serial Time :0.02257 Seconds
Parallel Time :0.0107966 Seconds

редактировать: После изменения, которое предложил Тимдей

Операционные системы : Linux Ubuntu 64 бит процессор : corei5

Serial Part
-----------
Mean of a :1.81203e-05
Mean of b :1.0324e-05
Standard deviation of a :0.707107
Standard deviation of b :0.707107
Pearson Correlation Coefficient: 3.65091e-07

Parallel Part
-------------
Mean of a :-0.000304446
Mean of b :0.00172593
Standard deviation of a :0.708465
Standard deviation of b :0.7039
Pearson Correlation Coefficient: -0.140716

Time Estimates
--------------
Serial Time :0.0235391 Seconds
Parallel time :0.00810775 Seconds

С уважением.

Примечание 1: Я не гарантирую, что приведенный выше фрагмент кода верен. Я верю в это.

Примечание 2: Этот фрагмент кода также был протестирован на Linux-системах.

Примечание 3: Были испробованы различные комбинации размеров зерна и параметры автоматического разделения.

4

Решение

Я глубоко подозреваю закомментированное /*,mean_a(0),mean_b(0)*/ в compute_mean( compute_mean& x, split) конструктор. Вероятно, ваши различия могут возникнуть из-за неинициализированных данных, загрязняющих результаты. Я предполагаю, что на машинах, где вы получаете последовательные результаты, разделение задач не происходит, или эти элементы просто находятся в нулевой памяти.

Точно так же ваш compute_sd( compute_sd& x, split) листья store3 а также store4 неинициализированный.

3

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

Это приводит к моим вопросам:

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

Помимо моментов в ответе Timday, ваши проблемы, кажется, не являются специфическими для параллелизма. Стабильные алгоритмы для вычислений с числами с плавающей запятой сложны для разработки; низкий детерминизм, который присущ эффективному использованию параллелизма, обнажает проблемы, которые всегда имели неадекватные алгоритмы. Смотрите ниже, что я имею в виду. Вы должны проверить надежность последовательного кода относительно порядка входных данных, прежде чем решить, будет ли параллелизм или алгоритм ответственны за числовую нестабильность.

В каких ситуациях мы не должны использовать параллельный подход, несмотря на наличие нескольких процессоров?

Когда в цикле недостаточно операций для оплаты накладных расходов. Это зависит от алгоритма, оборудования и размера проблемы.

Что лучше всего сделать, чтобы избежать ошибок округления? (Позвольте мне указать, что я говорю не о принудительном применении мьютексов и барьеров, которые иногда могут ограничивать степень параллелизма, а о простых советах по программированию, которые могут быть под рукой иногда)

Независимо от того, пишете ли вы последовательный или параллельный код, вы должны использовать алгоритмы, разработанные для обеспечения стабильности чисел. Те, кого вы учили в старшей школе, были разработаны для простоты понимания! 🙂 Например, см. http://en.m.wikipedia.org/wiki/Algorithms_for_calculating_variance.

1

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