==== Mátrix szorzás gyorsítása ====
A mátrixszorzás gyorsítása C-ben többféle módon lehetséges. Az alábbiakban bemutatunk néhány optimalizálási technikát:
=== Optimális sorrend ===
A hagyományos mátrixszorzás három beágyazott ciklusból áll (i, j, k sorrendben). Azonban a memóriaelérés optimalizálásával (pl. oszlopok helyett sorok szerint haladás) jelentősen növelhetjük a sebességet.
void multiplyMatrices(int **A, int **B, int **C, int N) {
for (int i = 0; i < N; i++) {
for (int k = 0; k < N; k++) { // Megváltoztatott sorrend
int r = A[i][k];
for (int j = 0; j < N; j++) {
C[i][j] += r * B[k][j]; // Csökkentett memóriahozzáférés
}
}
}
}
** Miért gyorsabb? **
* Az **A** mátrix sorait gyorsabban beolvassuk a CPU gyorsítótárba (cache).
* Az előre kiszámított **r** változó csökkenti az A[i][k] memória-hozzáférését.
=== Blokkosítás ===
A blokkosított mátrixszorzás során a mátrixokat kisebb blokkokra bontjuk, és ezeket a blokkokat külön-külön számoljuk ki, így kihasználva a CPU gyorsítótárát.
$$ A =
\begin{bmatrix}
1 & 2 & 3 & 4 \\
5 & 6 & 7 & 8 \\
9 & 10 & 11 & 12 \\
13 & 14 & 15 & 16
\end{bmatrix}$$
$$ B =
\begin{bmatrix}
1 & 0 & 0 & 1 \\
0 & 1 & 1 & 0 \\
1 & 0 & 0 & 1 \\
0 & 1 & 1 & 0
\end{bmatrix}$$
A fenti mátrixokat 2×2-es blokkokra bontjuk, tehát így:
$$ A =
\begin{bmatrix}
\begin{bmatrix} 1 & 2 \\ 5 & 6 \end{bmatrix} &
\begin{bmatrix} 3 & 4 \\ 7 & 8 \end{bmatrix} \\
\begin{bmatrix} 9 & 10 \\ 13 & 14 \end{bmatrix} &
\begin{bmatrix} 11 & 12 \\ 15 & 16 \end{bmatrix}
\end{bmatrix}$$
$$ B =
\begin{bmatrix}
\begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix} &
\begin{bmatrix} 0 & 1 \\ 1 & 0 \end{bmatrix} \\
\begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix} &
\begin{bmatrix} 0 & 1 \\ 1 & 0 \end{bmatrix}
\end{bmatrix}$$
=== Mátrix szorzása blokkonként ===
$$ C_{ij} = \sum_{k} A_{ik} \cdot B_{kj} $$
Ahol minden C blokk a megfelelő A és B blokkok szorzataként adódik.
Első blokk (C₁₁): $$C_{11} = A_{11} \cdot B_{11} + A_{12} \cdot B_{21}$$
$$ \begin{bmatrix} 1 & 2 \\ 5 & 6 \end{bmatrix} \cdot \begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix} +
\begin{bmatrix} 3 & 4 \\ 7 & 8 \end{bmatrix} \cdot \begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix}$$
Számoljuk ki az egyes részmátrixok szorzatát:
$$ \begin{bmatrix} 1\cdot1 + 2\cdot0 & 1\cdot0 + 2\cdot1 \\ 5\cdot1 + 6\cdot0 & 5\cdot0 + 6\cdot1 \end{bmatrix} =
\begin{bmatrix} 1 & 2 \\ 5 & 6 \end{bmatrix}$$
$$\begin{bmatrix} 3\cdot1 + 4\cdot0 & 3\cdot0 + 4\cdot1 \\ 7\cdot1 + 8\cdot0 & 7\cdot0 + 8\cdot1 \end{bmatrix} =
\begin{bmatrix} 3 & 4 \\ 7 & 8 \end{bmatrix}$$
Összeadva:
$$ \begin{bmatrix} 1 & 2 \\ 5 & 6 \end{bmatrix} + \begin{bmatrix} 3 & 4 \\ 7 & 8 \end{bmatrix} = \begin{bmatrix} 4 & 6 \\ 12 & 14 \end{bmatrix}$$
=== Végeredmény ===
Hasonlóan számolva az összes blokkra ez lesz a végeredmény:
$$ C =
\begin{bmatrix}
4 & 6 & 4 & 6 \\
12 & 14 & 12 & 14 \\
20 & 22 & 22 & 20 \\
28 & 30 & 30 & 28
\end{bmatrix} $$
Miért gyorsabb ez a módszer?
* Cache-hatékonyság: Egy blokk adatai könnyebben beleférnek a CPU gyorsítótárába.
* Kevesebb memória-hozzáférés: A kisebb méretű részmátrixok többször felhasználhatók anélkül, hogy újra és újra be kellene tölteni a RAM-ból.
* Jól párhuzamosítható: Az egyes blokkok párhuzamosan is számolhatók több CPU magon vagy GPU-n.
**C** implementáció:
void multiplyMatricesBlocked(int A[N][N], int B[N][N], int C[N][N]) {
// Eredménymátrix nullázása
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
C[i][j] = 0;
}
}
// Blokkosított szorzás
for (int bi = 0; bi < N; bi += BLOCK_SIZE) {
for (int bj = 0; bj < N; bj += BLOCK_SIZE) {
for (int bk = 0; bk < N; bk += BLOCK_SIZE) {
// Blokkon belüli műveletek
for (int i = bi; i < bi + BLOCK_SIZE; i++) {
for (int j = bj; j < bj + BLOCK_SIZE; j++) {
for (int k = bk; k < bk + BLOCK_SIZE; k++) {
C[i][j] += A[i][k] * B[k][j];
}
}
}
}
}
}
}