## Thread Affinity
如何從作業系統手中奪回「排程主導權」,解決在多核心環境下,執行緒在不同 CPU 核心間「遷移(Migration)」所造成的效能損失。
### Miss Table
| 現象 | 發生原因 | 對效能的影響 |
| :--- | :--- | :--- |
| **Cache Miss** | 數據不在新核心的快取裡 | 增加數百個週期的延遲。 |
| **TLB Miss** | 記憶體映射不在新核心裡 | 觸發 Page Walk,代價極高。 |
| **CPU Migration** | 排程器為了負載平衡強行搬家 | 導致上述兩者同時發生。 |
### Why Affinity
排程器為了負載平衡(Load Balancing),可能會將一個執行緒從 CPU 0 移到 CPU 2。
但只要一搬移,就會帶來兩個嚴重的研究課題:
1. **Cache Cold Start (快取冷啟動)**
原本在 CPU 0 的 L1/L2 快取數據,在 CPU 2 必須重新從 L3 或記憶體抓取。
> 當一個執行緒原本在 Core 0 跑,後來被作業系統遷移(Migrate)到 Core 1 時,Core 1 的 L1/L2 快取裡完全沒有這個執行緒剛才運算的數據。核心必須去 L3 快取甚至實體記憶體(RAM)把數據重新抓回來。這就是所謂的 Cache Invalidation(快取失效),而之所以是去L3,是因為On-demand策略,走L3比copy有效率
* Cache 失效:數據的「冷啟動」問題
每個 CPU 核心都有自己私有的 **L1 和 L2 Cache**。
* **發生了什麼事?** 當一個執行緒原本在 Core 0 跑,後來被作業系統遷移(Migrate)到 Core 1 時,Core 1 的 L1/L2 快取裡完全沒有這個執行緒剛才運算的數據。
* **代價:** 核心必須去 L3 快取甚至實體記憶體(RAM)把數據重新抓回來。這就是所謂的 **Cache Invalidation(快取失效)**。
2. **TLB Flush (Translation Lookaside Buffer, used to map VM to phy mem)**
分頁表快取失效,增加虛擬地址轉換的成本。
* TLB 重構
這是比 Cache 失效更重大的開銷。**TLB (Translation Lookaside Buffer)** 是 CPU 內部的一個小快取,專門用來存放「**虛擬地址到實體地址的轉換表**」。
* **為什麼要重構?**
1. 虛擬記憶體是「假的」,CPU 必須透過頁表(Page Table)才能找到實體 RAM。
2. 查表(Page Walk)很慢,所以 CPU 會把最近查過的結果存進 **TLB**。
3. **TLB 通常是核心私有的**。當執行緒從 Core 0 搬到 Core 1,Core 1 的 TLB 裡沒有該執行緒的地址轉換記錄。
* **代價:** CPU 必須重新去走訪記憶體裡的「多級頁表」來填滿 Core 1 的 TLB。
#### 但為什麼核心之間不能同時將TLB和CACHE搬過去?
* SRAM
Cache 在硬體上的位置。L1 和 L2 Cache 不是放在記憶體條上,而是直接蝕刻在 CPU 核心的電路裡面(SRAM)。這使他無法移動,只能複製,所以數據不是「搬」過去,而是要透過 CPU 內部總線或 Mesh Network從 Core A **複製**一份到 Core B。
* 能量
另外,大規模搬移數據是非常耗電且會產生高熱的動作。如果每次執行緒切換都要搬幾百 KB 的數據,CPU 會變得很燙。
> 能量我不熟,所以忽略,注重考慮的是從L3搬資料跟走Copy誰比較快
2. CPU Interconnect Occupied
* 數據量龐大
現代 CPU 的 L2 Cache 可能有 1MB 或更多。如果作業系統每秒進行數千次執行緒遷移,總線會滿載
* 優先權問題
總線連接太多東西了,這條路還要用來處理記憶體讀寫、核心間的通訊(如 MESI 協定)。如果被「搬家公司」佔滿了,其他的運算指令就會卡住。
> 那如果是cluster? core之間塞獨立的interconnect,每個core對外都有自己的線路?
> -> Area Problem?
3. **TLB跟 CPU 內部的 MMU(記憶體管理單元) 緊密結合**
所以不同狀況與不同經歷的CPU中的TLB架構會不一樣,直接拿過去會導致錯亂
#### 缺點1 負載不均(Load Imbalance)
如果你把 4 個重度運算的執行緒都綁在 Core 0,而 Core 1 到 Core 3 都在閒逛,你的程式會比沒設 Affinity 還慢。作業系統的排程器原本想幫你「平衡」,但被你禁用了。
#### 缺點2 核心拓撲(Topology)
現代 CPU 有 **超執行緒 (Hyper-threading)**。
* 如果你把兩個互相競爭 Cache 的執行緒綁在同一個實體核心的兩個邏輯核心上,它們會更擠。
* 如果你把需要大量通訊的執行緒綁在不同的 **NUMA Node**(不同插槽的 CPU),通訊延遲會炸掉。
### Affinity Code
```c
// ~/pthreads/affinity.c
// 執行時tmux或直接多開一個terminal,輸入htops,F2 setup-> Display option-> CPU detail
#define _GNU_SOURCE
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sched.h>
#include <stdbool.h>
#include <stdalign.h>
#define CORE 16
// -O3,編譯器會進行「暫存器優化」。
// 在這個執行緒的視角裡,沒有人會動這個變數,
// 所以不需要每次都去記憶體讀取,
// 直接把它放在 CPU 暫存器裡跑就好。
// 結果就是:就算主程式把 keep_running 改成 false 了,
// 子執行緒還是在看自己暫存器裡的舊值(true),永遠停不下來。
// 加上 volatile 是強制 CPU 每次都要回記憶體看最新的值。
volatile bool keep_running = true;
// 儲存每個執行緒的結果
typedef struct {
alignas(64) unsigned long long final_count;
int thread_id;
int bound_core;
} ThreadResult;
void* heavy_worker(void* arg) {
ThreadResult* result = (ThreadResult*)arg;
int core_to_bind = result->bound_core;
// 1. 設定執行緒親和性 (Affinity)
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(core_to_bind, &cpuset);
pthread_t current_thread = pthread_self();
if (pthread_setaffinity_np(current_thread, sizeof(cpu_set_t), &cpuset) != 0) {
perror("pthread_setaffinity_np");
return NULL;
}
// 2. 密集運算迴圈
unsigned long long local_count = 0;
while (keep_running) {
local_count++; // 單純的加法運算
}
result->final_count = local_count;
return NULL;
}
int main() {
int num_cores = sysconf(_SC_NPROCESSORS_ONLN);
printf("=== P-core vs E-core 算力競賽 ===\n");
printf("系統偵測到核心數: %d\n", num_cores);
printf("將啟動 %d 個執行緒,分別綁定到核心 0-%d\n", CORE, CORE-1);
printf("...\n\n");
pthread_t threads[CORE];
ThreadResult results[CORE];
// 3. 啟動執行緒,每個執行緒綁定到對應編號的核心
for (int i = 0; i < CORE; i++) {
results[i].thread_id = i;
results[i].bound_core = i; // 這裡你可以手動更改,例如只測 0 和 12
results[i].final_count = 0;
pthread_create(&threads[i], NULL, heavy_worker, &results[i]);
}
// 4. 固定測試時間
printf("正在運算中,請查看 htop...\n");
fflush(stdout);
getchar(); // 等待使用者按下 Enter 鍵來結束測試
keep_running = false; // 停止所有執行緒
// 5. 等待回收並打印結果
for (int i = 0; i < CORE; i++) {
pthread_join(threads[i], NULL);
}
printf("\n--- 實驗結果 (5秒總算力) ---\n");
printf("%-10s | %-10s | %-20s\n", "Thread ID", "Core ID", "Total Increments");
printf("--------------------------------------------------\n");
for (int i = 0; i < CORE; i++) {
printf("Thread %-2d | Core %-2d | %-20llu\n",
results[i].thread_id, results[i].bound_core, results[i].final_count);
}
return 0;
}
```
## Final Project (Phase 1)
Include : **Thread Lifecycle**、**Synchronization**、**False Sharing**、**Affinity**。
### 專案題目
多核心異構環境下的矩陣特徵提取器 **(Parallel Matrix Feature Extractor)**
### Spec
給定一個巨大的二維矩陣 $M$ ($16384 \times 16384$,單精度浮點數),你需要撰寫一個平行程式來計算:
1. **全局總和 (Global Sum)**:矩陣所有元素的累加。
2. **行極值 (Row-wise Maxima)**:每一行最大的數值,存入一個陣列。
3. **效能報告**:量化在不同核心綁定策略下的執行效率。
#### Requirement 1 : Domain Decomposition
* **任務**:將矩陣按「行 (Rows)」平均分配給 $N$ 個執行緒。
* **OS 連結**:利用 **Thread Affinity** 將每個執行緒固定在不同的 CPU 核心上,觀察 P-core 與 E-core 的計算速度落差。
#### Requirement 2 : Reduction Optimization
* **禁忌**:嚴禁多個執行緒直接對一個全域變數加鎖(會產生嚴重的 Contention)。
* **要求**:每個執行緒必須先計算其分配區域的「局部總和 (Local Sum)」,最後再彙整。
#### Requirement 3 : False Sharing Preventing
* **挑戰**:如果你建立一個儲存每個執行緒局部結果的陣列 `float results[NUM_THREADS]`,會觸發 False Sharing。
* **要求**:使用 **Padding** 或 **Thread-local Storage** 確保各核心的 L1 Cache 不會互相打架。
---
### File Structure
```text
parallel-project-01/
├── src/
│ ├── main.c # 負責初始化矩陣、建立執行緒、匯總結果
│ ├── worker.c # 核心計算邏輯 (含 Affinity 設定)
│ └── utils.h # 定義對齊用的結構體
├── scripts/
│ └── run_bench.sh # 自動測試 1, 2, 4, 8, 16 執行緒的腳本
├── Makefile # 需包含 -O3 與 -pthread
└── report.md # 你的研究發現 (核心數據分析)
```
---
### report
`report.md` 必須包含以下量化分析:
1. **加速比曲線 (Speedup Curve)**:
* 計算 $S = T_1 / T_n$。
* **分析**:當執行緒數量超過實體核心(Physical Cores)進入超執行緒(Hyper-threading)時,加速比是否趨於平緩?為什麼?
2. **硬體異構性分析 (Heterogeneity)**:
* 分別測試「只綁定 P-cores」與「只綁定 E-cores」的執行時間。
* **量化**:計算兩者的 **效能功耗比估算**(若有工具可測)或單純的運算速度倍率。
3. **Cache Miss 驗證**:
* 使用 `perf stat -e L1-dcache-load-misses` 比較「有 Padding」與「無 Padding」版本的數據差異。
### 啟動代碼片段 (Data Structure)
為了幫你起步,這裡提供一個符合研究所規範的記憶體對齊結構建議:
```c
#define CACHELINE_SIZE 64
typedef struct {
double local_sum;
// Padding 確保每個 struct 佔用整倍數的 Cache Line
char padding[CACHELINE_SIZE - sizeof(double)];
} __attribute__((aligned(CACHELINE_SIZE))) thread_data_t;
```
Project
https://hackmd.io/@1p0_VOUHTXGcHlBU9A0OEA/S1VgDyViWg
LAB 4 (Phase 2)
https://hackmd.io/@1p0_VOUHTXGcHlBU9A0OEA/ryUEZv55bx