Responsabili: Radu Ciobanu, Andrei Damian, Delia Stuparu, Dragoș Cocîrlea
În acest laborator, vom discuta despre câteva exemple de algoritmi paraleli de sortare.
Unul din cei mai cunoscuți (chiar dacă nu neapărat eficienți) algoritmi de sortare este bubble sort. Acesta funcționează pe baza următorului pseudocod:
function bubbleSort(list) { sorted = false; while (!sorted) { sorted = true; for (var i = 0; i < list.length - 1; i++) { if (list[i] > list[i + 1]) { swap(list[i], list[i + 1]); sorted = false; } } } }
Așa cum se poate observa mai sus, bubble sort parcurge șirul de sortat element cu element, comparând elementul curent cu vecinul din dreapta. Dacă numărul din dreapta este mai mic, se realizează o interschimbare între elementul curent și cel din dreapta sa. Complexitatea acestui algoritm este O(N2), pentru că se termină în cel mult N parcurgeri ale șirului (unde N este numărul de elemente din șirul de sortat).
Un exemplu de comportament al bubble sort se poate observa în imaginea de mai jos.
Dacă am dori să paralelizăm acest algoritm, un potențial mod de abordare ar fi să realizăm în paralel comparația și potențiala interschimbare de elemente vecine, dar acest lucru ar putea duce la problema prezentată în imaginea de mai jos.
Mai precis, operații pe elemente adiacente nu se pot realiza simultan, pentru că se poate ajunge la un race condition. Din acest motiv, un mod de a paraleliza bubble sort este dat de algoritmul odd-even transposition sort, care funcționează după următorul pseudocod:
function oddEvenSort(list) { for (var k = 0; k < list.length; k++) { for (i = 0; i < list.length - 1; i += 2) { if (list[i] > list[i + 1]) { swap(list[i], list[i + 1]); } } for (i = 1; i < list.length - 1; i += 2) { if (list[i] > list[i + 1]) { swap(list[i], list[i + 1]); } } } }
Așa cum se poate observa mai sus, odd-even transposition sort are două faze. În faza pară, elementele de pe poziții pare din șirul de sortat sunt comparate (și eventual interschimbate) cu vecinii din dreapta. După ce se termină faza pară (adică după ce toate elementele pare au fost procesate), urmează faza impară, în care elementele impare sunt analizate și comparate cu vecinii din dreapta. La fel ca la bubble sort, numărul maxim de iterații necesare pentru a sorta un șir va fi N (numărul de elemente din șir). Dacă avem P fire de execuție, complexitatea acestui algoritm va fi O(N/P*N), sau O(N) pentru P=N. În imaginea de mai jos, se poate observa o reprezentare grafică a modului de funcționare al odd-even transposition sort.
Un alt exemplu de algoritm de sortare care a fost conceput pentru sisteme multi-procesor unde un procesor este conectat doar la o parte din celelalte procesoare este shear sort (cunoscut de asemenea ca row-column sort sau snake-order sort), care presupune că lucrăm pe procesoare conectate într-o formă de matrice. Astfel, un procesor poate să comunice cu vecinii din stânga, din dreapta, de sus și de jos. Dacă ne imaginăm deci că procesoarele sunt așezate într-o matrice, cele două faze ale algoritmului shear sort sunt următoarele:
Se garantează că algoritmul va sorta numerele după cel mult sup(log2N) + 1 faze, unde N este numărul de elemente ce trebuie sortate. Din acest motiv, algoritmul are complexitatea O(Nlog2N). Pseudocodul algoritmului este prezentat mai jos.
function shearSort(matrix) { for (k = 0; k < ceil(log2(matrix.lines * matrix.columns)) + 1; k++) { for (i = 0; i < matrix.lines; i += 2) { sortAscendingLine(i); } for (i = 1; i < matrix.lines; i += 2) { sortDescendingLine(i); } for (i = 0; i < matrix.columns; i++) { sortAscendingColumn(i); } } }
O reprezentare grafică a funcționării shear sort este prezentată în figura de mai jos.
Merge sort (sau sortarea prin interclasare) este un algoritm de sortare de tip divide et impera care presupune următorii pași generali:
Numărul de pași de interclasare necesari este log2N, iar operațiile de interclasare de la un pas se realizează în O(N), deci complexitatea algoritmului merge sort este O(Nlog2N).
Pentru a paraleliza acest algoritm, putem observa că operațiile de interclasare de la fiecare pas se pot realiza în paralel. Totuși, operațiile de „merge” de la fiecare pas trebuie terminate în totalitate înainte de a trece la următorul pas, deci avem nevoie de o barieră (sau un mecanism similar) după fiecare pas de interclasare. Se poate observa că gradul de paralelism de la un pas de interclasări este din ce în ce mai mic pe măsură ce avansăm în algoritm, pentru că numărul de operații de „merge” de la fiecare pas scade. Complexitatea algoritmului paralel este O(N) pentru P=N.
O reprezentare grafică a algoritmului de merge sort paralel se poate observa în imaginea de mai jos, unde operațiile cu aceeași culoare pot fi realizate în paralel, iar simbolurile cu roșu reprezintă bariere.
Algoritmul de căutare binară paralelă se bazează pe împărțirea secvenței în care se face căutarea în P subsecvențe de aceeași lungime. La fiecare pas, fiecare din cele P procesoare verifică dacă o valoare x se află în subsecvența atribuită procesorului respectiv, prin compararea valorii lui x cu valorile aflate pe pozițiile start și end, care marchează capetele subsecvenței unui procesor.
Dacă un procesor a găsit valoarea x în subsecvența sa, acesta va anunța celelalte procesoare despre acest lucru, iar apoi toate cele P procesoare vor căuta în subsecvența respectivă, care va fi împărțită în alte P subsecvențe, căutarea trecând la pasul următor.
Spre deosebire de versiunea serială, unde se verifică dacă elementul căutat se află pe poziția mijlocie din secvență, la versiunea paralelizată se verifică dacă elementul se află pe una din pozițiile de start și de end din secvență (dacă se află pe una dintre aceste poziții, înseamnă că am găsit elementul în secvență).
Se poate observa că operațiile de căutare a valorii x în subsecvențe de către fiecare procesor de la fiecare pas se pot realiza în paralel. Totuși, toate operațiile de căutare de la un pas trebuie să fie finalizate în totalitate înainte de trecerea la următorul pas, așadar în acest caz este nevoie de o barieră după fiecare pas de căutare.
O reprezentare grafică a algoritmului de căutare binară paralelă se poate observa în imaginea de mai jos, unde operațiile cu aceeași culoare pot fi realizate în paralel, liniile de aceeași culoare reprezintă indicii start și end ai fiecărei subsecvențe, specifice fiecărui procesor, iar simbolurile cu roșu reprezintă bariere.
În toate fișierele sursă din scheletul de laborator, verificarea corectitudinii implementării paralele se realizează prin sortarea șirului folosind quick sort într-un array folosit ca etalon la final (se realizează o comparație element cu element). Atunci când doriți să verificați scalabilitatea programului vostru, comentați sau eliminați sortarea folosind quick sort și comparația aferentă, pentru a avea niște timpi cât mai corecți. De asemenea, încercați să eliminați toate afișările în terminal.
Implementarea merge sort din scheletul de laborator funcționează pentru un N putere a lui 2. Testați-vă implementarea paralelă doar pe acest caz.