Параллелизм с использованием 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 с Core i5 внутри. Он дал мне абсолютно одинаковые значения для всех параметров на выходе с параллельным кодом, намного быстрее, чем последовательный код. Вот мой результат:

ОС: 64-разрядная версия Windows 7 Ultimate Процессор: 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

ОС: 64-разрядная версия Linux Fedora Процессор: ядро ​​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. Были опробованы различные комбинации размера зерна и варианты автоматического разделения.


person sjsam    schedule 19.09.2012    source источник
comment
Не могли бы вы опубликовать результаты для одной или нескольких из этих архитектур? Мне неясно, говорите ли вы, что вы только получаете различия в результатах параллельного кода или больше различий в результатах параллельного кода, и какова величина этих различий. .   -  person timday    schedule 20.09.2012
comment
@timday: Спасибо за уделенное время. Я добавил больше результатов. Вы можете видеть, что все результаты в окнах LINUX показывают аномалии, но обратите внимание, что были некоторые окна LINUX, которые показали точные результаты, которые я не могу воспроизвести в настоящее время. У меня аномалии возникают только в параллельной части. Величина этих различий иногда невелика, но иногда и велика.   -  person sjsam    schedule 20.09.2012
comment
@sjsam ну, не очевидно, что не так, но, согласно вашему результату, вероятно, есть некоторые части, которые не совместимы с кросс-платформой, стандартное отклонение от ubuntu, похоже, дает совершенно неправильные результаты, я предлагаю вам использовать базовые функции, такие как sqrt и все остальные и сравните их, также size_t может иметь влияние. Также имеет значение, если вы используете одни и те же компиляторы. Кроме того, проверьте sizeof (double) на каждой платформе, если он меняется, это сильно повлияет на результат.   -  person Oliver    schedule 20.09.2012
comment
@OliverStutz: Спасибо за время. Поведение основных функций, таких как sqrt, действительно заслуживает внимания. Также у большинства наших платформ была архитектура Intel 64 (расширенный набор команд), где sizeof double составляет 8 байт.   -  person sjsam    schedule 20.09.2012
comment
Вы пробовали использовать одно и то же разложение read_array / compute_mean как для последовательной, так и для параллельной версий? Либо измените последовательную версию, чтобы инициализировать массив отдельно от вычисления средних значений, либо измените параллельную версию, чтобы инициализировать массив в том же цикле, который вычисляет среднее значение. Мне интересно, суммируют ли некоторые компиляторы с результатом с высокой точностью из sin (), а не с более низким значением точности из [k].   -  person Brangdon    schedule 20.09.2012
comment
@Brangdon: Я не пробовал одно и то же разложение для последовательной и параллельной частей. Более того, поскольку a [k] относится к типу double, который является типом возвращаемого значения sin (), как точность здесь будет проблемой ??   -  person sjsam    schedule 20.09.2012
comment
Последовательный код дает одинаковые результаты на всех платформах, а параллельный - нет: это довольно большой красный флаг, в параллельном коде есть некоторая проблема с синхронизацией / гонкой, и действительно, код просто кажется неправильным. использование TBB, которое уязвимо для выявления различий во времени, влияющих на поведение разделения TBB (см. мой ответ ниже). IMHO OpenMP был бы гораздо лучшим инструментом для создания параллельной версии представленного последовательного кода (TBB полезен в случаях, когда OpenMP не может справиться), но это действительно похоже на учебное упражнение для OP.   -  person timday    schedule 20.09.2012


Ответы (2)


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

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

person timday    schedule 20.09.2012
comment
Я склоняюсь к вашему первому предложению. Каждый раз, когда мы работаем с порциями данных, mean_a и mean_b должны быть обнулены. Я снял благодарность с этой строки (см. Правку). Но значения в store3 и store4, которые на самом деле являются константами внутри этой подпрограммы, переназначаются на mean_a и mean_b в ходе программы. Есть ли у них необходимость повторно инициализировать его после конструктора разделения compute_sd( compute_sd& x, split)? Кроме того, ответы параллельного кода более близки к ответам последовательного кода после предложенного вами редактирования. - person sjsam; 20.09.2012
comment
Эээ, мне это кажется ясным: у класса compute_sd есть члены store3 и store4. Если экземпляр compute_sd создается с использованием разделенной формы конструктора, то в созданном экземпляре store3 и store4 неинициализированы, и первый вызов operator () будет использовать неопределенные значения. Судя по последней части ваших комментариев выше, я считаю, что вы неправильно понимаете конструкторы C ++ и механизм разделения TBB. - person timday; 20.09.2012
comment
Ты прав ! Я должен был раньше понять, что конструктор разделения вызывает другой экземпляр compute_sd и создает другой стек переменных. Это заставило меня изменить эту строку следующим образом: 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){} - person sjsam; 21.09.2012

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

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

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

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

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

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

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

person mabraham    schedule 23.11.2013
comment
Прошло много времени с тех пор, как я разместил этот пост и почти забыл о нем. У меня были некоторые недопонимания относительно механизмов разделения TBB. Я собираюсь пересмотреть алгоритмы TBB и Parallel. Ваши предложения поступили точно в срок. Спасибо. - person sjsam; 28.11.2013