Laboratorul 3 - Algoritmi paraleli de sortare și de căutare

Responsabili: Radu Ciobanu, Andrei Damian, Delia Stuparu, Dragoș Cocîrlea

În acest laborator, vom discuta despre câteva exemple de algoritmi paraleli de sortare.

Odd-even transposition sort (OETS)

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.

Algoritmul odd-even transposition sort a fost gândit inițial pentru a fi rulat pe șiruri de procesoare (processor arrays), unde un procesor conține o singură valoare din șirul de sortat și poate comunica doar cu procesorul din stânga și cu cel din dreapta.

Shear 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 sortează liniile matricei astfel încât randurile pare au valorile ordonate crescător, iar rândurile impare au valorile ordonate descrescător
  • se sortează coloanele crescător.

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.

La finalul rulării algoritmului, lista de numere va fi sortată într-un mod „șerpuit”, de unde și numele algoritmului. Acest lucru se poate observa în imaginea de mai jos.

Merge sort paralel

Merge sort (sau sortarea prin interclasare) este un algoritm de sortare de tip divide et impera care presupune următorii pași generali:

  1. se împarte șirul de N elemente de sortat în N șiruri de lungime 1
  2. se aplica operația de interclasare („merge”) între câte două astfel de șiruri de lungime 1, rezultând N/2 șiruri sortate de lungime 2
  3. se repetă pașii de mai sus realizând interclasări între șiruri din ce în ce mai mari, până se ajunge la un șir sortat de N elemente.

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.

Căutarea binară paralelă

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.

Exerciții

  1. Pornind de la implementarea secvențială a algoritmului bubble sort aflată în fișierul oets.c din scheletul de laborator, implementați și paralelizați algoritmul odd-event transposition sort.
  2. Analizați scalabilitatea implementării de la punctul precedent. Verificați Hint 1 de mai jos pentru informații suplimentare.
  3. Pornind de la implementarea secvențială a algoritmului shear sort aflată în fișierul shear.c din scheletul de laborator, paralelizați algoritmul.
  4. Analizați scalabilitatea implementării de la punctul precedent. Verificați Hint 1 de mai jos pentru informații suplimentare.
  5. Pornind de la implementarea secvențială a algoritmului merge sort aflată în fișierul merge.c din scheletul de laborator, paralelizați algoritmul. Verificați Hint 2 de mai jos pentru informații suplimentare.
  6. Pornind de la scheletul de cod din fișierul parallel_binary_search.c din scheletul de laborator, care are o implementare secvențială a algoritmului de căutare binară ca hint, implementați versiunea paralelizată a căutarii binare. Analizați scalabilitatea implementării.

Hint 1

Î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.

Hint 2

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.

apd/laboratoare/03.txt · Last modified: 2022/10/24 09:12 by radu.ciobanu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0