--- title: 'CPU、記憶體、快取' disqus: kyleAlien --- CPU、記憶體、快取分頁快取 === ## OverView of Content [TOC] ## 儲存裝置 - 概述 記憶體也有分階層,其關係到記憶體的速度、大小、價位... 等等,請參考下圖 > ![](https://i.imgur.com/WqkgU1c.png) ### 快取記憶體 - SRAM * CPU 處理資料的流程 1. 將資料從記憶體讀取到 暫存器 2. CPU 運算暫存器上的資料 3. 將運算資料存回記憶體 > 總線:可以參考 [**C & 內存 & 操作系統**](https://hackmd.io/45lybpWGQxe4kmEdho8C2w?view#C-amp-%E5%85%A7%E5%AD%98-amp-%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%B5%B1) > > ![](https://i.imgur.com/bx01tta.png) 而處理資料的瓶頸之一就是從 `讀取記憶體資料到暫存器` & `暫存器存回記憶體`,因為記憶體的速度比暫存器慢很多 > 記憶體可能花 1ms > 暫存器只需 0.1ms (該數值不重要,重要的是相差速度) * 為了處理上述的資料傳輸速度差異,於是在 暫存器 跟 記憶體 之間出現了快取記憶體 SRAM,快取記憶體 1. 速度:記憶體 < 快取記憶體 < 暫存器 2. 大小:記憶體 > 快取記憶體 > 暫存器 :::info * 快取記憶體在哪? 一般內建於 CPU,但也有裝置是把快取記憶體至於 CPU 之外 ::: > ![](https://i.imgur.com/5wpg454.png) ### 快取記憶體 - Dirty Data * 知道為何需要快速記憶體後,我們來看看它是如何使用,如何加速資料的存取 1. 快取記憶體讀取資料到 `R0` 暫存器 > ![](https://i.imgur.com/jNFrMQg.png) 2. 再次讀取相同位址資料到 `R1` 暫存器 (**不會從記憶體,++直接透過快取記憶體++**,這樣加快了速度) > ![](https://i.imgur.com/lHKX03i.png) 3. CPU 修改 `R0` 暫存器資料為 `0xAA` > ![](https://i.imgur.com/eO8r7gz.png) 4. 將暫存器 `R0` 資料寫回快取記憶體,**並在快取記憶體上 ==標記 Dirty Data==** > ![](https://i.imgur.com/fB7Ws18.png) 5. 在特定時間用 **背景處理** 的方式寫回至記憶體,並清除 Dirty Data 標記 > ![](https://i.imgur.com/Ns9J1qc.png) :::success * 何時寫回 ? 背景處理 ? 這個方法被稱為 **分批寫回 (Write back)**,在快取記憶體變髒的瞬間,記憶還有名為寫入一次寫回的方式 (Write Throght) ::: ### 清理快取記憶體 - 震盪現象 * 當快取記憶體空間都有資料,但又要存取一筆新的資料到快取記憶體時,就會 **捨棄** 其中一筆的資料,這會有兩種情況 1. 清除正常資料:清除資料,寫入目標資料到記憶體 2. 清除的資料是 **Dirty Data**:先將資料寫回記憶體,在讀取目標資料到快取記憶體 > ![](https://i.imgur.com/Rd1Gi8U.png) :::danger * 震盪現象 若快取記憶體有過多 Dirty Data,則會造成快取記憶體不斷的在寫回、讀取,這會造成使用者卡頓現象 ::: ### 裝置快取記憶體 * 一般來說階層式快取記憶體取名會以 L 開頭 (Level),**L<數字> 數字越小,速度越快** > eg. L1 比 L2 快,L1 比 L2 小 * `x86_64` 架構的 cpu 是採用 階層式結構,不過 CPU 共用、延遲、大小都不會相同,可透過以下指令查看相關訊息 ```shell= ## 路徑: /sys/devices/system/cpu/cpu<編號>/cache/index<快取記憶體> # 查看 cpu0 的,L1 快取 cat /sys/devices/system/cpu/cpu0/cache/index0 ls -l ``` > ![](https://i.imgur.com/1Co9Jwq.png) | 檔案名稱 | 說明 | | -------- | -------- | | type | 快取種類,**Data 代表資料,Code 代表程式碼,Unified 以上兩者皆會快取** | | shared_cpu_list | 共用該快取的邏輯 cpu 列表 | | size | 大小 | | coherency_line_size | 快取列大小 | * 目前裝置 L1 快取 (index0) > ![](https://i.imgur.com/oEUj0ec.jpg) * 目前裝置 L4 快取 (index3) > ![](https://i.imgur.com/zRJFGM2.jpg) ### 快取記憶體 - 測試 * 測試目的:不同存取大小跟快取記憶體之間的關係,如果存取大小跟快取相同(或是倍數),是否會提高存取速度 ```c= #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/mman.h> #include <time.h> #include <err.h> // 快取列大小,對應 coherency_line_size #define CACHE_LINE_SIZE 64 // 測試迴圈 (固定) #define NLOOP (4 * 1024UL * 1024) // 計算時間 #define NSECS_PER_SEC 1000000000UL #define TP struct timespec static inline long diff_nsec(TP before, TP after) { return ( (after.tv_sec * NSECS_PER_SEC + after.tv_nsec) - (before.tv_sec * NSECS_PER_SEC + before.tv_nsec) ); } int main(int argc, char *argv[]) { char *progname = argv[0]; if(argc != 2) { fprintf(stderr, "usage: %s <size[KB]>\n", progname); exit(EXIT_FAILURE); } // Use cpu's register register int size; size = atoi(argv[1]) * 1024; if(!size) { fprintf(stderr, "size should be >= 1: %d\n", size); exit(EXIT_FAILURE); } char *buffer; buffer = mmap ( NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0 ); if(buffer == (void *) -1) { err(EXIT_FAILURE, "mmap() failed."); } TP before, after; clock_gettime(CLOCK_MONOTONIC, &before); int times = NLOOP / (size / CACHE_LINE_SIZE); printf("Target test: %d, times: %d\n", size, times); for(int i = 0; i < times; i++) { for(long j = 0; j < size; j += CACHE_LINE_SIZE) { buffer[j] = 0; } } clock_gettime(CLOCK_MONOTONIC, &after); printf("%f\n", (double) diff_nsec(before, after) / NLOOP); if(munmap(buffer, size) == -1) { err(EXIT_FAILURE, "munmap() failed."); } exit(EXIT_SUCCESS); } ``` * 編譯 & 運行程式:目前裝置 [**樹梅派 pi 400**](https://www.raspberrypi.com/documentation/computers/processors.html#bcm2711),**快取層級 L1: `32KB`、L2: `1M` (共用)** 1. 由於這次需要觀測的數值較小,所以編譯時多添加 `-O3` 最佳化 ```shell= # -O3 is optimize level cc -O3 -o cache cache.c # Test case size a=(4 8 16 32 64 128 256 512 1024 2048 4096 8192 16382 32768) # foreach array of a for i in ${a[@]}; do ./cache $i; done ``` 可以看到在寫入 4KB ~ 1024KB 使用的時間相當迅速 (1024KB 似乎就有點慢了 ? ),當一次寫入大於 1024KB 時間會大幅上升 > ![](https://i.imgur.com/ABBS3BD.png) :::info 從這邊可以看出如果一次寫入大於 樹梅派 pi 400 的 L2 快取記憶體 (1M),寫入速度就會降低,這也是震盪現象產生的問題 ::: ## 快取記憶體特性 ### 參照局部性 * 前面的實驗我們知道,快取記憶體可以加速運算的存取速度,因為大多數程式都具有 **==參照的局部性==**,這個局部性有兩個種類 1. **時間局部性**:於某個時間點被存取,不久後再次被存取的機率很高 > eg. for 迴圈、處理中的程式區域 ```java= int size = 20; for(int i = 0; i < size; i++) { println("Num: " + i); } ``` 2. **空間局部性**:當某個時間點存取到某些資料時,會對其附近記憶體位址進行的存取可能性比較高 > eg. Array 存取 `data[0]`,那其他的 `data[0] ~ [4]` 都算是空間局部性的範圍 ```java= int[] data = { 0, 123, 445, 667, 1235 }; ``` * 行程在考慮切割成短期間時,會傾向於對比本身所取得的記憶體量還要小很多的 **範圍存取** 因此這個 **存取範圍 的大小只要落在快取記憶體大小即可** > 這樣才能發揮快取的最大作用 ## CPU - TLB 區域 * TLB 的全名是 Translation Lookaside Buffer,它是 CPU 內的一個區塊,又名為 **==後備緩衝區==,該區塊有跟快取記憶體相同的速度** ### TLB 區域 * TLB 區塊的主要功能是將虛擬記憶體位址,轉換到實體記憶體位址。一般來說必須經過以下步驟 1. 透過分頁表項目上的參照 (Virtual : Real 對應的表),來將虛擬記憶體轉換為實體記憶體 2. 對實體記憶體進行存取 > ![](https://i.imgur.com/HLBSPo6.png) ## Appendix & FAQ :::info ::: ###### tags: `Linux 系統核心`