---
# System prepended metadata

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

---

## 指令級平行與編譯器自動化 (OpenMP)
首先，需要理解 OpenMP 如何在編譯時期（Compile-time）將簡單的指令轉換為複雜的執行緒管理邏輯，並掌握數據在並行區域間的流動

## Fork-Join
>當你在 C 語言中加入 #pragma omp parallel 時，編譯器背後到底做了什麼？

1. Fork-Join 執行流程
    * Fork：
    當主執行緒(Master)遇到並行指令時，會Fork出一組執行緒團隊（Team of Threads）。
    * Parallel Region：
    所有執行緒同時執行大括號內的程式碼。
    * Join：
    在並行區域結束處有一個「隱含的同步點（Implicit Barrier）」，所有執行緒匯合後銷毀，只剩下主執行緒繼續執行。

2. Data Sharing 
> pthread時，我們總需要一個`void* arg`，(worker wrap)

在 Pthreads 中，我們必須手動處理 `void* arg`；在 OpenMP 中，我們透過 Clauses 來定義變數的歸屬：

* **shared**
預設屬性。所有執行緒存取同一個記憶體位址（需注意 Race Condition）。
* **private**
每個執行緒在自己的 Stack 上建立一個同名變數的複本（初值未定義）。
* **firstprivate**
類似 private，但會繼承主執行緒中該變數的初始值。
* **reduction**
最常用的優化指令。它會為每個執行緒建立私有變數，並在 Join 時自動進行加總、相乘等操作，完全避開了手動加鎖的開銷。

### Sample Code : omp_basic
```c
// omp_basic.c
#include <omp.h>
#include <stdio.h>

#define TEST 0
int main() {
#if !TEST
    int total_sum = 0;
    int data[100];
    for(int i=0; i<100; i++) data[i] = 1;

    // 只需要這一行，編譯器就會幫你完成 Fork, Work-sharing, 與 Reduction
    #pragma omp parallel for reduction(+:total_sum)
    for (int i = 0; i < 100; i++) {
        total_sum += data[i];
    }

    printf("Total Sum: %d\n", total_sum);
    printf("預設執行緒數: %d\n", omp_get_max_threads());
    
#else 
    long long total_sum = 0;

    struct timespec m_start, m_end;
    clock_gettime(CLOCK_MONOTONIC, &m_start);

    // num_threads(THREADS): 強制指定使用 16 個執行緒
    // reduction(+:total_sum): 告訴編譯器幫我們做區域加總與合併
    #pragma omp parallel for num_threads(THREADS) reduction(+:total_sum)
    for (long long i = 1; i <= LIMIT; i++) {
        total_sum += i;
    }

    clock_gettime(CLOCK_MONOTONIC, &m_end);

    printf("計算完成！1 到 %lld 的總和是: %lld\n", LIMIT, total_sum);
    printf("執行時間: %f 秒\n", (m_end.tv_sec - m_start.tv_sec) + (m_end.tv_nsec - m_start.tv_nsec) / 1e9);
    printf("使用的執行緒數: %d\n", THREADS);

#endif
    return 0;
}

```
> 與~ /src/pthreads/thread_trace.c 做比較

```
pthread :

    計算完成！1 到 1000000000 的總和是: 500000000500000000

    執行時間: 0.034421 秒

    使用的執行緒數: 16

openmp :

    計算完成！1 到 1000000000 的總和是: 500000000500000000

    執行時間: 0.047312 秒

    使用的執行緒數: 16
```
### 推測
當下達 `#pragma omp parallel` 時，OpenMP 在底層必須做很多安全檢查：
1. 它要喚醒執行緒池 (Thread Pool )。
    > 於補充說明
2. 它要建立一個隱藏的 Barrier (屏障)，確保所有執行緒在迴圈結束時等待。
3. 它要處理 reduction 的記憶體對齊與安全合併機制。

這些是通用與保險所造成的 Overhead 
> 不過現實計算更複雜，此處只是用最簡易的加法累計來測量。
#### 觀察
```
# 可以產出一個，比assemblely好一點的展開code 
# 找到 __builtin_GOMP_parallel 這是fork的位置
gcc -O3 -fopenmp -fdump-tree-ompexp omp_basic.c -o omp_basic
```
### Tracing code
```c
// omp_trace.c
#include <omp.h>
#include <stdio.h>
#include <unistd.h>

void trace_info(const char* stage) {
    int id = omp_get_thread_num();
    int total = omp_get_num_threads();
    // 使用 printf 觀察執行緒的「分身」與「匯合」
    printf("[Thread %d/%d] Stage: %s\n", id, total, stage);
}

int main() {
    printf("--- 進入主執行緒 (Master Only) ---\n");
    trace_info("Sequential Start");

    printf("\n--- 觸發 #pragma omp parallel (Forking...) ---\n");
    #pragma omp parallel
    {
        // 這裡會印出多行，代表 Fork 成功
        trace_info("Inside Parallel Region");
        
        #pragma omp master
        {
            printf("  > 只有 Master 執行緒 (%d) 會跑這行\n", omp_get_thread_num());
        }
    }

    printf("\n--- 離開並行區域 (Joined) ---\n");
    trace_info("Sequential End");

    return 0;
}
```
或看環境變數
```
# env var
export OMP_DISPLAY_ENV=VERBOSE
./omp_basic

# unset env var
unset OMP_DISPLAY_ENV
export OMP_DISPLAY_ENV=FALSE

# extra visualize tool
Intel VTune / Score-P
```


## Scheduling
> 當**Load Imbalance**，則需要調整，但調整又會出現overhead。

### Scheduling Strategies

OpenMP 提供了四種主要的排程方式，用來處理不同的任務特性：

| 排程類型 | 運作機制 | 適用場景 | 開銷 (Overhead) |
| :--- | :--- | :--- | :--- |
| **`static (default)`**  | 在編譯/啟動時就平分好任務。 | 每個迴圈計算量幾乎相等。 | 最低 |
| **`dynamic`** | 執行緒算完一個「區塊」後，再去申請下一個。 | 任務計算量不可預測（如：分形計算、不規則矩陣）。 | 較高 (需同步隊列) |
| **`guided`** | 初始區塊大，隨後逐漸縮小。 | 兼顧 `dynamic` 的靈活與減少申請次數。 | 中等 |
| **`runtime`** | 由環境變數 `OMP_SCHEDULE` 決定。 | 實驗階段，測試哪種最快。 | 視設定而定 |

需要注意的是，`omp_get_max_threads()`可以看使用多少threads，而如果沒有事先
```
#pragma omp parallel for num_threads(THREADS) reduction(+:total_sum)
或
export OMP_NUM_THREADS=n
```
openmp會在確認完硬體核心數的情況下直接default = max_core

---

## Sample Code : imbalance 

```c
// omp_sched.c
#include <omp.h>
#include <stdio.h>
#include <unistd.h>

void heavy_work(int iterations) {
    // 模擬負載重計算：迴圈次數隨 i 增加
    // 避免優化
    volatile double dummy = 0;
    for (int i = 0; i < iterations * 1000000; i++) {
        dummy += 1.0;
    }
}

int main() {
    int n = 20;
    double start, end;

    // 測試指令：可以手動更換為 dynamic, 1 或 guided
    printf("--- 測試 Static Scheduling ---\n");
    start = omp_get_wtime();
    
    #pragma omp parallel for schedule(static, 1)
    for (int i = 0; i < n; i++) {
        int tid = omp_get_thread_num();
        printf("Thread %d 執行迭代 %2d\n", tid, i);
        heavy_work(i); // 迭代次數越多，計算越重
    }
    
    end = omp_get_wtime();
    printf("Static Time: %f s\n\n", end - start);

    return 0;
}
```

>　( < type>, <chunk_size>)

* `schedule(static)`
直接平均分配
* `schedule(static,1)`
輪流給，每次**chunk_size**個(Round-robin)
    > 注意 False Sharing，task無padding的情況下，64bytes cache會必定會引發false sharing
* `dynamic, 4`
    ```
    # recommend ! but only for auto 
    export OMP_SCHEDULE="dynamic,4"
    ./omp_sched
    ```
    調整 `chunk size` (後面的數字)，觀察這對效能的微小影響。


### static,4
```

--- 分配結果統計 ---
Thread 0 處理的迭代: [ 0 1 2 3 64 65 66 67 ] (總計: 8 次)
Thread 1 處理的迭代: [ 4 5 6 7 68 69 70 71 ] (總計: 8 次)
Thread 2 處理的迭代: [ 8 9 10 11 72 73 74 75 ] (總計: 8 次)
Thread 3 處理的迭代: [ 12 13 14 15 76 77 78 79 ] (總計: 8 次)
Thread 4 處理的迭代: [ 16 17 18 19 80 81 82 83 ] (總計: 8 次)
Thread 5 處理的迭代: [ 20 21 22 23 84 85 86 87 ] (總計: 8 次)
Thread 6 處理的迭代: [ 24 25 26 27 88 89 90 91 ] (總計: 8 次)
Thread 7 處理的迭代: [ 28 29 30 31 92 93 94 95 ] (總計: 8 次)
Thread 8 處理的迭代: [ 32 33 34 35 96 97 98 99 ] (總計: 8 次)
Thread 9 處理的迭代: [ 36 37 38 39 ] (總計: 4 次)
Thread 10 處理的迭代: [ 40 41 42 43 ] (總計: 4 次)
Thread 11 處理的迭代: [ 44 45 46 47 ] (總計: 4 次)
Thread 12 處理的迭代: [ 48 49 50 51 ] (總計: 4 次)
Thread 13 處理的迭代: [ 52 53 54 55 ] (總計: 4 次)
Thread 14 處理的迭代: [ 56 57 58 59 ] (總計: 4 次)
Thread 15 處理的迭代: [ 60 61 62 63 ] (總計: 4 次)

執行時間: 2.188987 秒
```
### dynamic,4
```
-- 分配結果統計 ---
Thread 0 處理的迭代: [ 60 61 62 63 ] (總計: 4 次)
Thread 1 處理的迭代: [ 8 9 10 11 72 73 74 75 ] (總計: 8 次)
Thread 2 處理的迭代: [ 48 49 50 51 ] (總計: 4 次)
Thread 3 處理的迭代: [ 56 57 58 59 ] (總計: 4 次)
Thread 4 處理的迭代: [ 24 25 26 27 88 89 90 91 ] (總計: 8 次)
Thread 5 處理的迭代: [ 20 21 22 23 84 85 86 87 ] (總計: 8 次)
Thread 6 處理的迭代: [ 36 37 38 39 ] (總計: 4 次)
Thread 7 處理的迭代: [ 32 33 34 35 96 97 98 99 ] (總計: 8 次)
Thread 8 處理的迭代: [ 44 45 46 47 ] (總計: 4 次)
Thread 9 處理的迭代: [ 28 29 30 31 92 93 94 95 ] (總計: 8 次)
Thread 10 處理的迭代: [ 4 5 6 7 68 69 70 71 ] (總計: 8 次)
Thread 11 處理的迭代: [ 16 17 18 19 80 81 82 83 ] (總計: 8 次)
Thread 12 處理的迭代: [ 12 13 14 15 76 77 78 79 ] (總計: 8 次)
Thread 13 處理的迭代: [ 52 53 54 55 ] (總計: 4 次)
Thread 14 處理的迭代: [ 40 41 42 43 ] (總計: 4 次)
Thread 15 處理的迭代: [ 0 1 2 3 64 65 66 67 ] (總計: 8 次)

執行時間: 2.456770 秒

chunk_size = 16 
-> 執行時間: 1.868869 秒
```
---

### 排程開銷 
`dynamic` 需要一個global table，threads每次領任務都要進行一次同步（Lock/Atomic）。如果每個任務本身極短（例如：單純的加法），`dynamic` 的申請時間甚至會比計算時間還長。

## 無法預測的for-loop
這裡是專門處理非線性數據結構（如樹、圖）或遞迴演算法時，在無法提前得知總 loop 次數時需要做的。
```
#basic
#pragma omp parallel

# Linear
#pragma omp parallel for schedule

# non-Linear
#pragma omp task
```
### Tasking Parallel
* 任務產生器
通常由 single thread 負責，不斷把任務丟進Work pool。
* Exe
其它 threads 會從pool裡拿出來work出來跑（這叫 Work-stealing）。
* key : `taskwait / taskgroup`
並行任務是 unsync。如果你希望等待再提交，你必須加上 #pragma omp taskwait，否則主執行緒會直接跑掉，導致結果錯誤。
### Task Granularity
建立一個 task 需要配置記憶體、放入隊列、由排程器調度。如果 $n$ 很小（例如 $n=5$），計算只需幾ns，但建立任務要幾百ns。這就是 細粒度開銷Fine-grained Overhead
### Cutoff 
> 當問題已經小到不值得分給別人做時，請自己算完

即，當task大於`cutoff`時，才進行平行化;反之則自己搞定。所以這裡，當cutoff = 0後，所有threads一拿到task，又開始分工，沒打算生產，這樣一路往下自爆，直到 **task = cutoff**

1. 全局溢出：任務被擠到 Global Queue
2. 鎖（Lock）的介入：為了保證資料正確（不讓兩個 Thread 搶到同一個任務），全域隊列必須加鎖。
3. 排隊死結

總結 `cutoff` 的角色

| 條件 | 動作 | 執行緒行為 |
| :--- | :--- | :--- |
| **$n > \text{cutoff}$** | **建立 Task** | **分工** |
| **$n \le \text{cutoff}$** | **執行 Serial** | **生產** |


### Code
```c
// omp_task.c
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>


long fib_serial(int n) {
    if (n < 2) return n;
    return fib_serial(n - 1) + fib_serial(n - 2);
}

// 平行任務版本
long fib_task(int n, int cutoff) {
    if (n < 2) return n;
    
    // 當小於 cutoff 時，停止產生 Task，改用序列計算
    if (n < cutoff) {
        return fib_serial(n);
    }

    long x, y;
    #pragma omp task shared(x)
    x = fib_task(n - 1, cutoff);

    #pragma omp task shared(y)
    y = fib_task(n - 2, cutoff);

    #pragma omp taskwait
    return x + y;
}

int main() {
    int n, cutoff;
    printf("請輸入 Fibonacci 項數 (建議 30-40): ");
    if (scanf("%d", &n) != 1) return 1;
    printf("請輸入 Task Cutoff 閾值 (建議 20，輸入 0 代表全平行): ");
    if (scanf("%d", &cutoff) != 1) return 1;

    double start, end;
    long result;

    // --- 1. 測量純序列時間 ---
    printf("\n[1] 執行純序列計算...\n");
    start = omp_get_wtime();
    result = fib_serial(n);
    end = omp_get_wtime();
    printf("序列結果: %ld | 耗時: %f 秒\n", result, end - start);

    // --- 2. 測量平行任務時間 ---
    printf("\n[2] 執行 OpenMP Task 計算 (Cutoff=%d)...\n", cutoff);
    start = omp_get_wtime();
    #pragma omp parallel
    {
        #pragma omp single
        result = fib_task(n, cutoff);
    }
    end = omp_get_wtime();
    printf("平行結果: %ld | 耗時: %f 秒\n", result, end - start);

    return 0;
}
```
### Test
```
htop 

n = 40, cutoff = 0
```
#### htop 顯示 CPU 使用率很低?
> 大家都在分工，誰在那邊跟你玩計算。


由 **「任務生成的傳遞速度」** 與 **「執行緒等待策略（Wait Policy）」** 共同造成的。

##### 100% core
當你執行 $F_{40}$ 且 `cutoff=0` 時，任務是從一個 **單一入口**（Master Thread）開始產生的。

* **發包者（Generators）**：一開始只有一個 Thread 在執行 `fib(40)`。它會迅速產出 `fib(39)` 和 `fib(38)`。
* **擴散過程**：Thread 2 偷走了 `fib(39)`，Thread 3 偷走了 `fib(38)`。這幾位「先搶到任務」的執行緒，會立刻**狂發包**。
* **為什麼 100%**：這些執行緒正處於「產生任務 -> 嘗試塞進隊列 -> 失敗 -> 旋轉等待（Spinning）-> 成功 -> 產生下一個任務」的無限循環中。對於作業系統來說，這些執行緒一直處於「活動（Active）」狀態，所以顯示 100%。

##### 5% core
> 為什麼其它執行緒不去幫忙？

* **鎖競爭（Lock Contention）崩潰**
OpenMP 的Global Task Queue
* **OS 的介入**
當 Thread 5 想要去偷任務時，發現門口已經擠滿了 Thread 1~4，且鎖被鎖住了。
* **掛起（Suspend）**
如果 Thread 5 嘗試幾次都失敗，作業系統核心（Kernel）會先把它**移出執行隊列**
* **為什麼 5%**
在 `htop` 裡，這代表該執行緒大部分時間都在「睡眠（Sleep/Blocked）」，只有偶爾被喚醒起來看看有沒有機會搶到鎖，搶不到又立刻回去睡。


##### OpenMP 的等待策略：Active vs. Passive

這與 OpenMP 的內部設定 `OMP_WAIT_POLICY` 有關：

* **ACTIVE**：執行緒沒事做時會一直繞圈圈（Spin）等任務。這會讓所有核心都顯示 100%，但效率極低。
* **PASSIVE**：執行緒沒事做時會交回控制權給 OS 去睡覺。這就是你看到的 5% 現象。
* **混合狀態**：現代 OpenMP 通常會先讓你「旋轉一小段時間」，如果還是沒事做，就送你去睡覺。

**所以你看到的 `htop` 景象其實是：**
> 有幾個執行緒運氣好，搶到了發包的權利，忙得不可開交（100%）；而大多數執行緒則被擋在「任務隊列」的鎖外面，被 OS 強制送去休息了（5%）。

## 補充
* Threads Pool 
對應到`lifecycle.c`中所談到的create/delete overhead，將刪除(銷毀)改成睡眠，使Threads不會像竹筍一樣。另外，threads的lifecycle是取決於拿到的task何時結束，所以只要在task加while-loop，threads就會不被銷毀。
> 設計條件  
> 1. Task Queue
> 2. While-Loop
> 3. Synchronization
> * (Prevent) Mutex
> * (Prevent) Condition Variable

LAB 5 
https://hackmd.io/@1p0_VOUHTXGcHlBU9A0OEA/HkMJabKi-l