Россия, г. Москва |
Самостоятельная работа 2: Оптимизация вычислительно трудоемкого программного модуля для архитектуры Intel Xeon Phi. Линейные сортировки
Слияние "Разделяй и властвуй"
Идея слияния по алгоритму "Разделяй и властвуй" заключается в разбиении массивов на участки, которые можно слить независимо [ [ 4.8 ] ]. В первом массиве выбирается центральный элемент x (он разбивает массив на две равные половины), а во втором массиве с помощью бинарного поиска находится позиция наибольшего элемента меньшего x (позиция этого элемента разбивает второй массив на две части). После такого разбиения первые и вторые половины массивов могут сливать независимо, т.к. в первых половинах находятся элементы меньшие элемента x, а во второй – большие ( рис. 4.9). Для слияния двух массивов несколькими потоками можно в первом массиве выбрать несколько ведущих элементов, разделив его на равные порции, а во втором массиве найти соответствующие подмассивы. Каждый поток получит свои порции на обработку.
Эффективность такого слияние во многом зависит от того, насколько равномерно произошло "разделение" второго массива.
Создайте пустой файл main.cpp и скопируйте в него код из файла main_lsd3.cpp.
Класс Splitter выполняет слияние двух отсортированных массивов.
class Splitter:public task { private: double *mas1; double *mas2; double *tmp; int size1; int size2; public: Splitter(double *_mas1, double *_mas2, double *_tmp, int _size1, int _size2): mas1(_mas1), mas2(_mas2), tmp(_tmp), size1(_size1), size2(_size2) {} task* execute() { int a = 0; int b = 0; int i = 0; while( (a != size1) && (b != size2)) { if(mas1[a] <= mas2[b]) { tmp[i] = mas1[a]; a++; } else { tmp[i] = mas2[b]; b++; } i++; } if (a == size1) { int j = b; for(; j<size2; j++, i++) tmp[i] = mas2[j]; } else { int j=a; for(; j<size1; j++, i++) tmp[i] = mas1[j]; } return NULL; } };
Класс LSDParallelSorter реализует рекурсивный алгоритм слияния, выполняя последовательную сортировку, в том случае, если размер сортируемой порции массива меньше, чем значение поля portion (значение этого поля задаётся при создании объекта в функции LSDParallelSortDouble). Слияние двух отсортированных массивов начинается с разбиения массивов на порции (для этого используется метод BinSearch(), реализующий бинарный поиск). Далее создаются задачи для слияния каждой пары полученных подмассивов и выполняется их параллельный запуск на выполнение.
class LSDParallelSorter:public task { private: double *mas; double *tmp; int size; int portion; int threads; private: int BinSearch(double *mas, int l, int r, double x) { if(l==r) return l; if(l+1==r) if(x<mas[l]) return l; else return r; int m = (l+r)/2; if(x<mas[m]) r = m; else if(x>mas[m]) l=m; else return m; return BinSearch(mas, l, r, x); } public: LSDParallelSorter(double *_mas, double *_tmp, int _size, int _portion, int _threads): mas(_mas), tmp(_tmp), size(_size), portion(_portion), threads(_threads) {} task* execute() { if(size <= portion) { LSDSortDouble(mas, tmp, size); } else { LSDParallelSorter &sorter1 = *new (allocate_child()) LSDParallelSorter(mas, tmp, size/2, portion, threads/2); LSDParallelSorter &sorter2 = *new (allocate_child()) LSDParallelSorter(mas + size/2, tmp + size/2, size - size/2, portion, threads/2); set_ref_count(3); spawn(sorter1); spawn_and_wait_for_all(sorter2); Splitter **sp = new Splitter*[threads-1]; int s = size/2; s /= threads; int l = 0, r = s; int l2 = 0, r2; for(int i=0; i<threads-1; i++) { double x = mas[r]; r2 = BinSearch(mas + size/2, 0, size - size/2, x); sp[i] = new (allocate_child()) Splitter(mas+l, mas + size/2 + l2, tmp+l+l2, r-l, r2-l2); l += s; r += s; l2 = r2; } Splitter &spl = *new (allocate_child()) Splitter(mas+l, mas + size/2 + l2, tmp+l+l2, size/2-l, size - size/2 - l2); set_ref_count(threads+1); for(int i=0; i<threads-1; i++) spawn(*(sp[i])); spawn_and_wait_for_all(spl); for(int i=0; i<size; i++) mas[i] = tmp[i]; delete[] sp; } return NULL; } };
В функцию LSDParallelSortDouble() необходимо внести небольшие изменения, т.к. теперь в конструктор класса LSDParallelSorter передаётся количество потоков.
void LSDParallelSortDouble(double *inp, int size, int nThreads) { double *out=new double[size]; int portion = size/nThreads; if(size%nThreads != 0) portion++; LSDParallelSorter& sorter = *new (task::allocate_root()) LSDParallelSorter(inp ,out, size, portion, nThreads); task::spawn_root_and_wait(sorter); delete[] out; }
Соберите получившуюся реализацию и проведите тест для 10 миллионов элементов при разном числе потоков. Результаты, полученные авторами на тестовой инфраструктуре, представлены на рис. 4.10.
![Результаты параллельной побайтовой восходящей сортировки при использовании слияния "Разделяй и властвуй" на Intel Xeon Phi](/EDI/16_08_14_2/1408137470-25747/tutorial/1169/objects/4/files/4_10.gif)
Рис. 4.10. Результаты параллельной побайтовой восходящей сортировки при использовании слияния "Разделяй и властвуй" на Intel Xeon Phi
Здесь мы запускали только вариант со случайным заполнением, но с разным числом потоков. Результаты очень похожи на предыдущие. Для наглядности приведём графики времени сортировки 10 миллионов ( рис. 4.11) и 100 миллионов ( рис. 4.12) элементов с помощью параллельного алгоритма LSD с использованием слияния "Разделяй и властвуй" на сопроцессоре.
увеличить изображение
Рис. 4.11. Время сортировки 10 миллионов элементов с помощью параллельного алгоритма LSD с использованием слияния "Разделяй и властвуй" на Intel Xeon Phi
увеличить изображение
Рис. 4.12. Время сортировки 100 миллионов элементов с помощью параллельного алгоритма LSD с использованием слияния "Разделяй и властвуй" на Intel Xeon Phi
Максимальное ускорение, равное 19.7, достигается при использовании 129 потоков.