This shows you the differences between two versions of the page.
asc:laboratoare:09 [2024/04/28 14:13] emil.slusanschi [Resurse] |
asc:laboratoare:09 [2025/05/06 20:14] (current) alexandru.bala [Accesul la vectori] |
||
---|---|---|---|
Line 76: | Line 76: | ||
{{:asc:lab5:aij.jpg|}} | {{:asc:lab5:aij.jpg|}} | ||
- | Astfel, pentru ''N = 6, M = 4: a[2][3] = a[0][0] + 2*6 + 3 = a[0][0] + 15'' | + | Astfel, pentru ''N = 6, M = 4: @a[2][3] = @a[0][0] + 2*6 + 3 = @a[0][0] + 15'' |
In limbaje de programare ca FORTRAN-ul, formula este inversata, deoarece aceste limbaje salvează vectorii în format column-major: | In limbaje de programare ca FORTRAN-ul, formula este inversata, deoarece aceste limbaje salvează vectorii în format column-major: | ||
- | ''a[i][j] = a[0][0] + j*M + i'' | + | ''@a[i][j] = @a[0][0] + j*M + i'' |
Oricare ar fi asezarea vectorilor in memorie, accesele la vectori sunt scumpe din punctul de vedere al performantelor. Noi vom considera de aici inainte o asezare row-major, ca in limbajul C. Conform acestei formule, pentru vectori bidimensionali (matrice), fiecare acces presupune doua adunari si o inmultire (de numere intregi). Evident, pentru vectori cu mai multe dimensiuni, aceste costuri cresc considerabil. Astfel, in momentul in care compilatorul intalneste instructiunea: | Oricare ar fi asezarea vectorilor in memorie, accesele la vectori sunt scumpe din punctul de vedere al performantelor. Noi vom considera de aici inainte o asezare row-major, ca in limbajul C. Conform acestei formule, pentru vectori bidimensionali (matrice), fiecare acces presupune doua adunari si o inmultire (de numere intregi). Evident, pentru vectori cu mai multe dimensiuni, aceste costuri cresc considerabil. Astfel, in momentul in care compilatorul intalneste instructiunea: | ||
Line 86: | Line 86: | ||
''suma += a[i][k] * b[k][j]'' | ''suma += a[i][k] * b[k][j]'' | ||
- | se vor efectua implicit, suplimentar inmultirii si adunarii in virgula mobila implicata de codul de mai sus, patru adunari si doua inmultiri in numere intregi pentru a calcula adresele necesare din vectorii a si b. Se intampla astfel destul de frecvent ca procesorul sa nu aiba date disponibile pentru a lucra in continuu, din cauza faptului ca overhead-ul pentru calculul adreselor este semnificativ. | + | se vor efectua implicit, suplimentar inmultiri si adunari in virgula mobila implicata de codul de mai sus, patru adunari si doua inmultiri in numere intregi pentru a calcula adresele necesare din vectorii a si b. Se intampla astfel destul de frecvent ca procesorul sa nu aiba date disponibile pentru a lucra in continuu, din cauza faptului ca overhead-ul pentru calculul adreselor este semnificativ. |
Astfel, un mod de a spori viteza programului este renuntarea la accesele vectoriale prin derefentiere utilizand in acest scop pointeri. De exemplu: | Astfel, un mod de a spori viteza programului este renuntarea la accesele vectoriale prin derefentiere utilizand in acest scop pointeri. De exemplu: | ||
Line 105: | Line 105: | ||
</code> | </code> | ||
- | In mod similar se procedeaza si pentru cazul in care indexul incrementat este cel al liniilor si nu cel al coloanelor. In ambele cazuri, practic se va calcula "de mana" adresa in cadrul vectorului, exact in modul in care ar face-o compilatorul limbajului folosit. Totusi, rezolvarea noastra este mai rapida, deoarece ea tine cont de pozitia in care ne aflam in cadrul vectorului, lucru destul de complicat de facut automat. De exemplu, pentru a trece la urmatoarea coloana, e suficient sa adunam N pointer-ului, fata de recalcularea pornind de la @(a[0][0]) ce necesita doua inmultiri si o adunare in intregi. Evident, facilitatile oferite de limbaje ca C-ul, ne vin in ajutor: astfel incrementarile de pointeri de tip char * vor face incrementarea cu un byte, in vreme ce pentru int * se va face cu patru bytes. Ca urmare a aspectelor prezentate mai sus, iata forma optimizata in care ajunge algoritmul nostru: | + | In mod similar se procedeaza si pentru cazul in care indexul incrementat este cel al liniilor si nu cel al coloanelor. In ambele cazuri, practic se va calcula "de mana" adresa in cadrul vectorului, exact in modul in care ar face-o compilatorul limbajului folosit. Totusi, rezolvarea noastra este mai rapida, deoarece ea tine cont de pozitia in care ne aflam in cadrul vectorului, lucru destul de complicat de facut automat. De exemplu, pentru a trece la urmatoarea coloana, e suficient sa adunam N pointer-ului, fata de recalcularea pornind de la @(a[0][0]) ce necesita doua adunari si o inmultire in intregi. Evident, facilitatile oferite de limbaje ca C-ul, ne vin in ajutor: astfel incrementarile de pointeri de tip char * vor face incrementarea cu un byte, in vreme ce pentru int * se va face cu patru bytes. Ca urmare a aspectelor prezentate mai sus, iata forma optimizata in care ajunge algoritmul nostru: |
<code cpp> | <code cpp> | ||
Line 279: | Line 279: | ||
</code> | </code> | ||
- | Pentru (Wc) avem acum (N/b)*(N/b)*b*b Cache-miss-uri, in vreme ce pentru (Ra) si (Rb) avem (N/b)*(N/b)*(N/b)*b*b, astfel ducand la N<sup>2</sup> + 2N<sup>3</sup>/b -> 2N<sup>3</sup>/b Cache-miss-uri pentru intregul algoritm. Combinand acest calcul cu faptul ca avem 2N<sup>3</sup> operatii aritmetice, rezulta un raport r = 2N<sup>3</sup>/b / 2N<sup>3</sup> -> b. Dupa cum am stabilit, r trebuie sa fie maxim (mai mare oricum decat 2-ul obtinut in varianta anterioara). Daca mergem pana la cazul extrem, il vom face pe b = N, dar asta nu este viabil, pentru ca atunci suntem din nou la cazul fara blocuri, de la care tocmai venim... | + | Pentru (Wc) avem acum (N/b)*(N/b)*b*b Cache-miss-uri, in vreme ce pentru (Ra) si (Rb) avem (N/b)*(N/b)*(N/b)*b*b, astfel ducand la N<sup>2</sup> + 2N<sup>3</sup>/b -> 2N<sup>3</sup>/b Cache-miss-uri pentru intregul algoritm. Combinand acest calcul cu faptul ca avem 2N<sup>3</sup> operatii aritmetice, rezulta un raport r = 2N<sup>3</sup> / 2N<sup>3</sup>/b -> b. Dupa cum am stabilit, r trebuie sa fie maxim (mai mare oricum decat 2-ul obtinut in varianta anterioara). Daca mergem pana la cazul extrem, il vom face pe b = N, dar asta nu este viabil, pentru ca atunci suntem din nou la cazul fara blocuri, de la care tocmai venim... |
Astfel, acest algoritm functioneaza doar daca blocurile intra in Cache. Acest lucru inseamna ca trei blocuri diferite, de dimensiune b*b, trebuie sa intre in Cache, pentru toate cele trei matrice (a, b si c). Daca C este dimensiunea Cache-ului in elemente de matrice, atunci trebuie sa fie 3b<sup>2</sup> ≤ C sau b ≤ √(C / 3) . Astfel, in cel mai bun caz, r-ul trebuie sa fie si el √(C / 3). | Astfel, acest algoritm functioneaza doar daca blocurile intra in Cache. Acest lucru inseamna ca trei blocuri diferite, de dimensiune b*b, trebuie sa intre in Cache, pentru toate cele trei matrice (a, b si c). Daca C este dimensiunea Cache-ului in elemente de matrice, atunci trebuie sa fie 3b<sup>2</sup> ≤ C sau b ≤ √(C / 3) . Astfel, in cel mai bun caz, r-ul trebuie sa fie si el √(C / 3). | ||
Line 381: | Line 381: | ||
} | } | ||
</code> | </code> | ||
+ | |||
+ | * [[https://www.linkedin.com/posts/eric-vyacheslav-156273169_this-is-the-best-matmul-animation-on-the-activity-7317502251646758913-mggn/?utm_source=share&utm_medium=member_android&rcm=ACoAADXXE9UB_GxnGDBMJ5zHUnOXdPwsl36scYY | O vizualizare foarte inspirata a BMM-ului.]] | ||
+ | |||
+ | * [[https://github.com/wentasah/mmul-anim/tree/master | Repo-ul de Github al vizualizarii BMM]] | ||
+ | |||
===== In loc de concluzie ===== | ===== In loc de concluzie ===== | ||
Line 424: | Line 429: | ||
* {{:asc:lab5:what_every_programmer_should_know_about_memory_by_ulrich_drepper_.pdf|What every programmer should know about memory.pdf}} | * {{:asc:lab5:what_every_programmer_should_know_about_memory_by_ulrich_drepper_.pdf|What every programmer should know about memory.pdf}} | ||
+ | |||
+ | |||
==== Valgrind ==== | ==== Valgrind ==== |