---
# System prepended metadata

title: Parallel Programming and System LAB5
tags: [SP]

---

## CPU 效能調優與演算法設計
> htop在 LAB 4 中顯示了Imbalance的問題，怎麼解?
```c
請輸入 Fibonacci 項數 (建議 30-40): 40
請輸入 Task Cutoff 閾值 (建議 20，輸入 0 代表全平行): 0

[1] 執行純序列計算...
序列結果: 102334155 | 耗時: 0.146667 秒

[2] 執行 OpenMP Task 計算 (Cutoff=0)...
平行結果: 102334155 | 耗時: 215.439894 秒
```

## Benchmark : 矩陣乘法 (Naive MatMul)
```c
# WSL2 perf 被 ban，所以只能用這個，然後不要跑太大的數字
sudo apt update && sudo apt install valgrind -y
valgrind --tool=cachegrind ./matmul_naive

# Check
export OMP_NUM_THREADS=16 #(default)
echo $OMP_NUM_THREADS

# EXE
gcc fopenmp filename.c -o filename 
```
### Code 2D matrix
```c
// matmul_naive_2D.c
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define N 1000

// 分配不連續的指標陣列
double** alloc_2d(int n) {
    double **mat = (double **)malloc(n * sizeof(double *));
    for (int i = 0; i < n; i++) {
        mat[i] = (double *)malloc(n * sizeof(double));
    }
    return mat;
}

void free_2d(double **mat, int n) {
    for (int i = 0; i < n; i++) free(mat[i]);
    free(mat);
}

void init_2d(double **mat, int n) {
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            mat[i][j] = (double)rand() / RAND_MAX;
        }
    }
}

int main() {
    // 1. 分配分散的記憶體 (Array of Pointers)
    double **A = alloc_2d(N);
    double **B = alloc_2d(N);
    double **C = alloc_2d(N);

    // C 矩陣需要手動歸零
    for(int i=0; i<N; i++) {
        for(int j=0; j<N; j++) C[i][j] = 0.0;
    }

    srand(time(NULL));
    init_2d(A, N);
    init_2d(B, N);

    printf("--- 2D 指標陣列版本 (N=%d) ---\n", N);
    double start = omp_get_wtime();
                // 隱含雙重位址查找：先去記憶體找 A[i] 的位址，再去抓 [k] 的值
                sum += A[i][k] * B[k][j];
            }
            C[i][j] = sum;

    // 2. 核心計算
    #pragma omp parallel for collapse(2)
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            double sum = 0;
            for (int k = 0; k < N; k++) {
        }
    }

    double end = omp_get_wtime();
    printf("2D 版本耗時: %.4f 秒\n\n", end - start);

    free_2d(A, N); free_2d(B, N); free_2d(C, N);
    return 0;
}
```
#### Return
```c
2D 版本耗時: 78.9607 秒

==71421== 
==71421== I   refs:      30,200,266,781
==71421== I1  misses:             1,855
==71421== LLi misses:             1,827
==71421== I1  miss rate:           0.00%
==71421== LLi miss rate:           0.00%
==71421== 
==71421== D   refs:      14,086,594,242  (13,066,393,883 rd   + 1,020,200,359 wr)
==71421== D1  misses:       972,517,644  (   971,137,411 rd   +     1,380,233 wr)
==71421== LLd misses:           585,570  (        82,572 rd   +       502,998 wr)
==71421== D1  miss rate:            6.9% (           7.4%     +           0.1%  )
==71421== LLd miss rate:            0.0% (           0.0%     +           0.0%  )
==71421== 
==71421== LL refs:          972,519,499  (   971,139,266 rd   +     1,380,233 wr)
==71421== LL misses:            587,397  (        84,399 rd   +       502,998 wr)
==71421== LL miss rate:             0.0% (           0.0%     +           0.0%  )
```
### Code 2D -> 1D matrix  
```c
// matmul_naive_1D.c
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define N 1024 // 矩陣維度

// 初始化矩陣
void init_matrix(double *mat, int size) {
    for (int i = 0; i < size * size; i++) {
        mat[i] = (double)rand() / RAND_MAX;
    }
}

int main() {
    // 分配記憶體 (使用一維陣列模擬 N*N)
    double *A = (double *)malloc(N * N * sizeof(double));
    double *B = (double *)malloc(N * N * sizeof(double));
    double *C = (double *)calloc(N * N, sizeof(double)); // 初始化為 0

    if (!A || !B || !C) {
        printf("記憶體分配失敗！\n");
        return 1;
    }

    srand(time(NULL));
    init_matrix(A, N);
    init_matrix(B, N);

    printf("開始矩陣乘法 (N=%d, Threads=%d)...\n", N, omp_get_max_threads());
    
    double start = omp_get_wtime();

    // --- Naive MatMul 核心邏輯 ---
    #pragma omp parallel for collapse(2)
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            double sum = 0;
            for (int k = 0; k < N; k++) {
                // A[i][k] -> A[i*N + k] (橫向，順著走)
                // B[k][j] -> B[k*N + j] (縱向，跨 N 個元素跳著走)
                sum += A[i * N + k] * B[k * N + j];
            }
            C[i * N + j] = sum;
        }
    }

    double end = omp_get_wtime();
    printf("計算完成。耗時: %.4f 秒\n", end - start);

    // 簡單校驗 (防止編譯器優化掉整個迴圈)
    printf("結果範例 C[0]: %f\n", C[0]);

    free(A); free(B); free(C);
    return 0;
}
```
```c
1D 版本耗時: 65.4077 秒

==71886== 
==71886== I   refs:      26,173,962,133
==71886== I1  misses:             1,837
==71886== LLi misses:             1,798
==71886== I1  miss rate:           0.00%
==71886== LLi miss rate:           0.00%
==71886== 
==71886== D   refs:      12,074,077,168  (11,055,055,664 rd   + 1,019,021,504 wr)
==71886== D1  misses:     1,127,252,106  ( 1,126,001,300 rd   +     1,250,806 wr)
==71886== LLd misses:           453,660  (        77,940 rd   +       375,720 wr)
==71886== D1  miss rate:            9.3% (          10.2%     +           0.1%  )
==71886== LLd miss rate:            0.0% (           0.0%     +           0.0%  )
==71886== 
==71886== LL refs:        1,127,253,943  ( 1,126,003,137 rd   +     1,250,806 wr)
==71886== LL misses:            455,458  (        79,738 rd   +       375,720 wr)
==71886== LL miss rate:             0.0% (           0.0%     +           0.0%  )
```

### 推測
1. 快取缺失率（Miss Rate）並不正比於最終執行時間
> 2D由於是`malloc`的產物，隨機分布於heap中，相當於前方有做了padding?
> 1D則是沒有padding，所以可能導致sharing false?
> 如果推論為正確，那padding所下降的miss rate是很可觀的，甚至比是否連續還重要，當然這是對這個scale而言 
2. 速度最終還是由1D快了些許，或許表示miss rate所帶來的penalty跟cache conflict比起來還低? 像TLB的cost可能就比想像中的高?
3. `#pragma omp parallel for collapse(2)`的優化

總之，目前的cache 配置導致每讀取一個 double，快取線 (Cache Line) 就會失效一次。所以要透過轉置來解決

### 轉置
源自於C code 對矩陣記憶體的排列為 Row-major ，使 $$sum += A[i][k] * B[k][j]$$
這類矩陣乘法在k增加時，B矩陣的存取跨越是一整列，可預測的是，miss rate會降低不少。
>另外，在優化後且不使用compile優化的情況下，16/1 的threads數量並無太大影響
> 額外補充，上述的測試中，LL皆會是0，極有可能是因為L3可以裝入完整的矩陣，故只測量最後一層cache (L3) 的miss rate 為0
```c
// matmul_naive_1TD.c
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define N 1000

void init_1d(double *mat, int size) {
    for (int i = 0; i < size * size; i++) {
        mat[i] = (double)rand() / RAND_MAX;
    }
}

int main() {
    double *A = (double *)malloc(N * N * sizeof(double));
    double *B = (double *)malloc(N * N * sizeof(double));
    double *B_T = (double *)malloc(N * N * sizeof(double)); // 用於存放轉置後的 B
    double *C = (double *)calloc(N * N, sizeof(double));

    srand(time(NULL));
    init_1d(A, N);
    init_1d(B, N);

    printf("--- 1D 轉置優化版本 (N=%d) ---\n", N);
    double start = omp_get_wtime();

    // 1. 矩陣轉置階段 (這部分開銷極小，但收益巨大)
    #pragma omp parallel for collapse(2)
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            B_T[i * N + j] = B[j * N + i];
        }
    }

    // 2. 核心計算階段 可自行加入num_threads(THREADS)在for後，但測試後16與1的差距並不大
    #pragma omp parallel for collapse(2)
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            double sum = 0;
            for (int k = 0; k < N; k++) {
                // 現在 A[i*N + k] 和 B_T[j*N + k] 都是連續記憶體存取！
                sum += A[i * N + k] * B_T[j * N + k];
            }
            C[i * N + j] = sum;
        }
    }

    double end = omp_get_wtime();
    printf("轉置版本耗時: %.4f 秒\n", end - start);

    free(A); free(B); free(B_T); free(C);
    return 0;
}
```
#### Return
```
轉置版本耗時: 51.7340 秒
==75983== 
==75983== I   refs:      26,197,966,489
==75983== I1  misses:             1,854
==75983== LLi misses:             1,815
==75983== I1  miss rate:           0.00%
==75983== LLi miss rate:           0.00%
==75983== 
==75983== D   refs:      12,084,079,749  (11,064,056,980 rd   + 1,020,022,769 wr)
==75983== D1  misses:       126,629,175  (   126,128,340 rd   +       500,835 wr)
==75983== LLd misses:           627,563  (       126,821 rd   +       500,742 wr)
==75983== D1  miss rate:            1.0% (           1.1%     +           0.0%  )
==75983== LLd miss rate:            0.0% (           0.0%     +           0.0%  )
==75983== 
==75983== LL refs:          126,631,029  (   126,130,194 rd   +       500,835 wr)
==75983== LL misses:            629,378  (       128,636 rd   +       500,742 wr)
==75983== LL miss rate:             0.0% (           0.0%     +           0.0%  )
```

## Tiling
> 當矩陣過大，那就切割吧，至少要切到剛剛好L1或L2 !?

### Tiled Matmul Code
即便我們轉置了矩陣，讓存取變得連續，但當 $N$ 變得很大時（例如 $N=2048$ 或更大），整個矩陣的尺寸會遠遠超過你的 **L1/L2 快取**容量。

* **目前的困境**：在計算完矩陣的一列後，當你計算下一列時，之前讀入快取的數據可能已經被「擠掉」了。這意味著同樣的數據會不斷地在 RAM 與 Cache 之間重複搬運。
* **分塊的邏輯**：將大矩陣切成一個個小方塊（Tile），大小剛好能**完全塞進 L1 或 L2 快取**。在處理這個小方塊時，所有的運算都在快取內完成，徹底消除對記憶體頻寬的依賴。



---

這會將原本的三層迴圈（$i, j, k$）擴展為**六層迴圈**。外三層決定現在處理哪一個「塊」，內三層負責塊內的計算。


> 這裡我們假設分塊大小為 `B` (Block Size)，調整進行測量

```c
#include <stdio.h>
#include <stdlib.h>
#include <omp.h>
#include <time.h>

// 定義矩陣大小與分塊大小
#define N 1000
#define B 8  // Block Size: 根據 L1 Cache 大小調整 (32x32 double 約 8KB)

// 輔助函式：取最小值
#define MIN(a, b) ((a) < (b) ? (a) : (b))

void init_matrix(double *mat, int size) {
    for (int i = 0; i < size * size; i++) {
        mat[i] = (double)rand() / RAND_MAX;
    }
}

int main() {
    // 1. 記憶體分配 (1D 連續空間)
    double *A   = (double *)malloc(N * N * sizeof(double));
    double *B_m = (double *)malloc(N * N * sizeof(double)); // 原始 B
    double *B_T = (double *)malloc(N * N * sizeof(double)); // 轉置後的 B
    double *C   = (double *)calloc(N * N, sizeof(double));  // 初始化為 0

    if (!A || !B_m || !B_T || !C) {
        printf("記憶體分配失敗！\n");
        return -1;
    }

    srand(time(NULL));
    init_matrix(A, N);
    init_matrix(B_m, N);

    printf("--- 最終優化版本: 1D + Transpose + Tiling (N=%d, B=%d) ---\n", N, B);
    double start_time = omp_get_wtime();

    // 2. 矩陣轉置 (將 B 轉置為 B_T，確保計算時為行連續存取)
    #pragma omp parallel for collapse(2)
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            B_T[i * N + j] = B_m[j * N + i];
        }
    }

    // 3. 核心計算：六層迴圈 (Tiled Matrix Multiplication)
    // 外三層迴圈：以「塊」為單位移動
    #pragma omp parallel for collapse(2)
    for (int i = 0; i < N; i += B) {
        for (int j = 0; j < N; j += B) {
            for (int k = 0; k < N; k += B) {
                
                // 內三層迴圈：在「塊」內部進行計算
                for (int ii = i; ii < MIN(i + B, N); ii++) {
                    for (int jj = j; jj < MIN(j + B, N); jj++) {
                        double sum = 0;
                        // 這裡 A 和 B_T 都是 Row-major 連續存取，極利於 Cache L1 與 SIMD
                        for (int kk = k; kk < MIN(k + B, N); kk++) {
                            sum += A[ii * N + kk] * B_T[jj * N + kk];
                        }
                        C[ii * N + jj] += sum;
                    }
                }

            }
        }
    }

    double end_time = omp_get_wtime();
    printf("優化版本耗時: %.4f 秒\n", end_time - start_time);

    // 驗證計算（可選，抽取一個值檢查）
    // printf("C[500]: %f\n", C[500]);

    free(A);
    free(B_m);
    free(B_T);
    free(C);

    return 0;
}
```

---

### 如何決定 Block Size ?

這不是猜出來的，而是根據硬體規格計算出來的。

1.  **查詢快取大小**：
    在 Linux 下執行 `lscpu` 或 `cat /proc/cpuinfo`。
    * 例如：L1 Data Cache = 32 KB。
2.  **計算容量限制**：
    在計算一個 $B \times B$ 的塊時，我們通常需要同時容納 A 的一塊、B 的一塊（以及 C 的一塊）。
    * 如果是 `double` (8 Bytes)，則 $3 \times B^2 \times 8 \text{ Bytes} \leq \text{L1 Cache Size}$。
    * 若 L1 為 32 KB，則 $B \approx 32$ 是個不錯的起點。
### Return 
```
// B = 32 with -O3
耗時: 8.7287 秒
==82241== 
==82241== I   refs:      5,192,939,004
==82241== I1  misses:            2,010
==82241== LLi misses:            1,944
==82241== I1  miss rate:          0.00%
==82241== LLi miss rate:          0.00%
==82241== 
==82241== D   refs:      1,132,077,286  (1,082,979,224 rd   + 49,098,062 wr)
==82241== D1  misses:        9,969,797  (    9,593,296 rd   +    376,501 wr)
==82241== LLd misses:          628,051  (      251,857 rd   +    376,194 wr)
==82241== D1  miss rate:           0.9% (          0.9%     +        0.8%  )
==82241== LLd miss rate:           0.1% (          0.0%     +        0.8%  )
==82241== 
==82241== LL refs:           9,971,807  (    9,595,306 rd   +    376,501 wr)
==82241== LL misses:           629,995  (      253,801 rd   +    376,194 wr)
==82241== LL miss rate:            0.0% (          0.0%     +        0.8%  )
```


```
// B = 64 without -O3
耗時: 58.0920 秒, (add -O3 耗時: 8.0809 秒, but miss rate increase)
==82745== 
==82745== I   refs:      31,826,330,518
==82745== I1  misses:             2,030
==82745== LLi misses:             1,966
==82745== I1  miss rate:           0.00%
==82745== LLi miss rate:           0.00%
==82745== 
==82745== D   refs:      13,348,682,436  (12,283,394,572 rd   + 1,065,287,864 wr)
==82745== D1  misses:         7,935,741  (     7,559,300 rd   +       376,441 wr)
==82745== LLd misses:           630,322  (       254,130 rd   +       376,192 wr)
==82745== D1  miss rate:            0.1% (           0.1%     +           0.0%  )
==82745== LLd miss rate:            0.0% (           0.0%     +           0.0%  )
==82745== 
==82745== LL refs:            7,937,771  (     7,561,330 rd   +       376,441 wr)
==82745== LL misses:            632,288  (       256,096 rd   +       376,192 wr)
==82745== LL miss rate:             0.0% (           0.0%     +           0.0%  )
```
END
LAB 6 
https://hackmd.io/@1p0_VOUHTXGcHlBU9A0OEA/rkqFUXtoZx