C OpenMP параллельная quickSort

еще раз я застрял при использовании openMP в C++. На этот раз я пытаюсь реализовать параллельный алгоритм быстрой сортировки.

код:

#include <iostream>
#include <vector>
#include <stack>
#include <utility>
#include <omp.h>
#include <stdio.h>

#define SWITCH_LIMIT 1000

using namespace std;

template <typename T>
void insertionSort(std::vector<T> &v, int q, int r)
{
    int key, i;
    for(int j = q + 1; j <= r; ++j)
    {
        key = v[j];
        i = j - 1;
        while( i >= q && v[i] > key )
        {
            v[i+1] = v[i];
            --i;
        }
        v[i+1] = key;
    }
}

stack<pair<int,int> > s;

template <typename T>
void qs(vector<T> &v, int q, int r)
{
    T pivot;
    int i = q - 1, j = r;
    //switch to insertion sort for small data
    if(r - q < SWITCH_LIMIT) 
    {
        insertionSort(v, q, r);
        return;
    }

    pivot = v[r];
    while(true)
    {
        while(v[++i] < pivot);
        while(v[--j] > pivot);
        if(i >= j) break;
        std::swap(v[i], v[j]); 
    }
    std::swap(v[i], v[r]);

    #pragma omp critical
    {
        s.push(make_pair(q, i - 1));
        s.push(make_pair(i + 1, r));        
    }
}

int main()
{
    int n, x;
    int numThreads = 4, numBusyThreads = 0;
    bool *idle = new bool[numThreads];
    for(int i = 0; i < numThreads; ++i)
        idle[i] = true;
    pair<int, int> p;
    vector<int> v;
    cin >> n;
    for(int i = 0; i < n; ++i)
    {
        cin >> x;
        v.push_back(x);
    }
    cout << v.size() << endl;
    s.push(make_pair(0, v.size()));

    #pragma omp parallel shared(s, v, idle, numThreads, numBusyThreads, p) 
    {
        bool done = false;
        while(!done) 
        {
            int id = omp_get_thread_num();
            #pragma omp critical
            {
                if(s.empty() == false && numBusyThreads < numThreads) 
                {
                    ++numBusyThreads;
                    //the current thread is not idle anymore
                    //it will get the interval [q, r] from stack
                    //and run qs on it
                    idle[id] = false;
                    p = s.top();                    
                    s.pop();
                }
                if(numBusyThreads == 0)
                {
                    done = true;
                }
            }
            if(idle[id] == false)
            {

                qs(v, p.first, p.second);
                idle[id] = true;
                #pragma omp critical 
                --numBusyThreads;
            }

        }
    }
    return 0;
}

:

чтобы использовать openMP для рекурсивной функции, я использовал стек для отслеживания следующих интервалов, на которых должна выполняться функция qs. Я вручную добавляю 1-й интервал [0, size] , а затем позволяю потокам работать, когда новый интервал добавляется в стек.

проблема:

программа заканчивается слишком рано, не сортируя массив после создания 1-го набора интервалов ([q, i - 1], [i+1, r], Если вы посмотрите на код. Я предполагаю, что потоки, которые получают работу, считают локальные переменные функции quicksort(qs в коде) общими по умолчанию, поэтому они путают их и не добавляют интервал в стек.

как я компилирую:

g++ -o qs qs.cc -Wall -fopenmp

Как Я беги:

./qs < in_100000 > out_100000

где in_100000-это файл, содержащий 100000 на 1-й строке, за которым следуют 100k intergers на следующей строке, разделенной пробелами.

Я использую gcc 4.5.2 в linux

Спасибо за помощь

Дэн

1 ответов


я на самом деле не запускал ваш код, но я вижу немедленную ошибку на p, которая должна быть private не shared. Параллельный вызов qs: qs(v, p.first, p.second); будет рас p, что приводит к непредсказуемому поведению. Локальные переменные в qs должно быть нормально, потому что все потоки имеют свой собственный стек. Однако общий подход хорош. Ты на правильном пути.


вот мои общие замечания по реализации параллельной quicksort. Самой быстрой сортировки параллельной, что означает, что синхронизация не требуется. Рекурсивные вызовы qs на секционированный массив параллельной.

однако параллелизм обнаруживается в рекурсивные форма. Если вы просто используете вложенные параллелизм в OpenMP, у вас будет тысяча потоков в секунду. Ускорение не будет достигнуто. Итак, в основном вам нужно превратить рекурсивный алгоритм в интерактивного общения один. Затем вам нужно реализовать своего рода work-queue. Это ваш подход. И это нелегко.

для вашего подхода есть хороший ориентир: OmpSCR. Вы можете скачать на http://sourceforge.net/projects/ompscr/

в бенчмарке есть несколько версий quicksort на основе OpenMP. Большинство из них похожи на ваши. Однако, чтобы увеличить параллелизм, необходимо свести к минимуму конкуренцию в глобальной очереди (в вашем коде это s). Таким образом, может быть несколько оптимизаций, таких как наличие локальных очередей. Хотя сам алгоритм является чисто параллельным, для реализации могут потребоваться артефакты синхронизации. И, самое главное, очень трудно получить ускорение.


однако вы по-прежнему напрямую используете рекурсивный параллелизм в OpenMP двумя способами: (1) дросселирование общего числа потоков и (2) Использование OpenMP 3.0 task.

вот псевдо-код для первого подхода (Это основано только на эталоне OmpSCR):

void qsort_omp_recursive(int* begin, int* end)
{
  if (begin != end) {
    // Partition ...

    // Throttling
    if (...)  {
      qsort_omp_recursive(begin, middle);
      qsort_omp_recursive(++middle, ++end);
    } else {

#pragma omp parallel sections nowait
      {
#pragma omp section
        qsort_omp_recursive(begin, middle);
#pragma omp section
        qsort_omp_recursive(++middle, ++end);
      }
    }
  }
}

чтобы запустить этот код, вам нужно позвонить omp_set_nested(1) и omp_set_num_threads(2). Код очень прост. Мы просто создаем две нити для разделения работы. Однако мы вставляем простую логику дросселирования, чтобы предотвратить избыточные потоки. Обратите внимание, что мои эксперименты показали приличные ускорения для этого подхода.


наконец, вы можете использовать OpenMP 3.0 task, где задача является логически параллельной работой. В над всеми подходами OpenMP каждая параллельная конструкция порождает два физическая потоки. Вы можете сказать, что между задачей и рабочим потоком существует жесткое сопоставление 1-к-1. Однако,task разделяет логические задачи и работников.

поскольку OpenMP 3.0 еще не популярен, я буду использовать Параметрами По Умолчанию Плюс, что отлично подходит для выражения такого рода вложенных и рекурсивных параллелизмов. В Cilk Plus распараллеливание чрезвычайно просто:

void qsort(int* begin, int* end)
{
  if (begin != end) {
    --end;
    int* middle = std::partition(begin, end,
      std::bind2nd(std::less<int>(), *end));
    std::swap(*end, *middle);

    cilk_spawn qsort(begin, middle);
    qsort(++middle, ++end);
    // cilk_sync; Only necessay at the final stage.
  }
}

я скопировал этот код из примера кода Cilk Plus'. Вы увидите одно ключевое слово cilk_spawn это все, чтобы распараллелить quicksort. Я пропускаю объяснения ключевого слова Cilk Plus и spawn. Однако это легко понять: два рекурсивных вызова объявляются как логически параллельные задачи. Всякий раз, когда происходит рекурсия, создаются логические задачи. Но среда выполнения Cilk Plus (которая реализует эффективный планировщик кражи работы) будет обрабатывать все виды грязной работы. Это оптимально очереди параллельные задачи и сопоставления с рабочими потоками.

обратите внимание, что OpenMP 3.0 task по существу аналогичен подходу Cilk Plus. Мои эксперименты показывают, что довольно хорошие ускорения были возможны. Я получил ускорение 3~4x на 8-ядерном компьютере. И ускорение было масштабным. Абсолютные ускорения Cilk Plus больше, чем у OpenMP 3.0.

подход Cilk Plus (и OpenMP 3.0) и ваш подход по существу одинаковы: разделение параллельной задачи и назначение рабочей нагрузки. Однако его очень трудно эффективно реализовать. Например, необходимо уменьшить разногласия и использовать структуры данных без блокировки.