# 2017q1 Homework3 (software-pipelining) contributed by < `ect276` > ###### tags: `嵌入式` **[code here]("https://github.com/etc276/prefetcher")** ## 開發環境 - Ubuntu 16.10 ( 64 bit ) ``` Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian CPU(s): 4 On-line CPU(s) list: 0-3 Thread(s) per core: 2 Core(s) per socket: 2 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 61 Model name: Intel(R) Core(TM) i7-5500U CPU @ 2.40GHz Stepping: 4 CPU MHz: 2400.878 CPU max MHz: 3000.0000 CPU min MHz: 500.0000 BogoMIPS: 4788.90 Virtualization: VT-x L1d cache: 32K L1i cache: 32K L2 cache: 256K L3 cache: 4096K NUMA node0 CPU(s): 0-3 ``` ## 預期目標 * 學習計算機結構並且透過實驗來驗證所學 * 理解 prefetch 對 cache 的影響,從而設計實驗來釐清相關議題 * 論文閱讀和思考 ## 論文閱讀 ### 研究目標 --- 探討 SW(軟體) 和 HW(硬體) 在 prefetch(預取) 對於效能影響的機制與交互作用 ### 名詞解釋 --- - cache (一種 memory) ![](https://i.imgur.com/REMWcJM.png) 由於 CPU 的處理速度大於 RAM 的,所以藉由將等下會用到的資料先放到 cache,以彌補之間的速度差。而因為 cache 的空間較小,所以 prefetch 的技術更顯得重要,才能有效降低 cache miss,而提升效能。 - latency > cache 對速度有什麼影響呢?這可以由 latency 來表示。CPU 在從記憶體中讀取資料(或程式)時,會需要等待一段時間,這段時間就是 latency,通常用 cycle 數表示。 資料來源:[CPU 的 cache 和 latency [Part 1]]("https://www.csie.ntu.edu.tw/~r89004/hive/cache/page_1.html") - overhead overhead 可理解為額外的 penalty,也就是當 cache miss 發生時,才會有的多餘時間,相對於 latency 是總共的 (就算 cache hit 也會有) 資料來源:[記憶體系統-快取記憶體]("http://systw.net/note/af/sblog/more.php?id=252") - cache miss vs. cache pollution cache miss 是結果,也就是 CPU 實際預存取的時候是失敗的。而 cache pollution 是原因,例如過早存取 (early prefetch) 以至於實際要用時已被 replaced,或是預先存取了用不到的data (incorrect prefetch) ### 1. Introduction --- 各種 SW 和 HW 組合對效能的影響 ( x 軸是各種 benchmark) ![](https://i.imgur.com/Clq8W42.png) ### 2. BACKGROUND ON SOFTWARE AND HARDWARE PREFETCHING --- 不同資料結構與其相對應的 prefetch 的方法 ![](https://i.imgur.com/2MrnrdY.png) - stream:英文原意為"流",可以先理解為 row major 然後走 row - stride:英文原意為"邁",也就是踏一大步的意思,可以先理解為 走 col - RDS 在 HW 上就會需要用到指標的幫忙了,並不單純為硬體 - Hash 在 HW 上只能倚賴平行化或預先計算來 prefetch,我理解為不能純硬體了 #### 2.1 Software Prefetch Intrinsics - intrinsic (內建函式):function call 時會根據 standard library 編譯成 built-in 的程式碼 (有點像 memset 這個 function 會被編譯成怎麼樣的組語,或是以下) ![](https://i.imgur.com/cpQx4tB.png) 參考資料:[What are intrinsics?]("http://stackoverflow.com/questions/2268562/what-are-intrinsics") - inline (內嵌、內聯函式):可以藉由人工或給編譯器建議,將 function 展開以減少 overhead (在 complie 階段將 function 轉成 instruction cache),用在少量程式碼的 function 可提升效能,但過於多的 source code 可能會導致 cache miss. :::danger intrinsics 不是 inline functions,不要搞錯! --jserv ::: #### 2.2 Prefetch Classification 分類 prefetch 的各種情況, timely 最優 ![](https://i.imgur.com/fA8Iowk.png) ![](https://i.imgur.com/ETESIdb.png) #### 2.3 Software Prefetch Classification ![](https://i.imgur.com/7csS1yL.png) >This value D is called the prefetch distance. We define prefetch distance as the distance ahead of which a prefetch should be requested on a memory address 由於 prefetch 的時候會 將記憶體的資料寫進 cache line 裡,也就是其實一次的 prefetch 可能不只抓一筆資料,所以選擇 prefetch distance 就顯得重要,因為若是距離太小,會造成無謂的 prefetch;距離太大則會造成 cache miss (應該要抓進來的資料被跳過了)。 prefetch 其實就是為了"隱藏" latency(預先存取,造成下次要讀取資料不需要時間的假象),所以 D 要求大於 l,不然就沒有隱藏的效果了。 > 至於除以 s 的用意還不理解,只知道跟 loop 的路徑有關,等待實驗分析後再看看。 #### 2.4 Direct and Indrect Memory Indexing - SW 在 indirect 的 prefetch 表現比 HW 好 - HW通常處理可預測的 stream, stride 模式 - indirect 可能會造成 SW prefetch overhead (e.g. 兩次 prefetch a[b[i]] ) ### 3. POSTIVE AND NEGATIVE IMPACTS OF SOFTWARE PREFETCHING --- #### 3.1 Benefits of Software Prefetching over Hardware Prefetching SW 相對於 HW 的優勢通常為較自由,能根據不同情況做調整 - Large Number of Streams (Limited Hardware Resources) - HW 能處理的 stream prefetch 受限於硬體 - 如 stream detectors和book-keeping mechanisms - 而 SW 的 stream 則可以根據不同要求做調整 - Short Streams - HW 的 prefetch 需要訓練 - 太短的 stream 就無法訓練 - 且 HW 需要 2 個 cache miss 才會調整方向 - Irregular Memory Access - Cache Locality Hint - 程式開發者使用SW prefetcher時,能自行調整 locality hint - HW prefetcher 的設計上則是大多將prefetch的資料搬移至lower-level cache ( L2 或 L3 ) - 優點: 減少 L1 cache的 cache pollution - 缺點: lower-level cache 資料搬移至 L1 的時間(latency)會造成效能下降 - Loop Bounds - SW 可以確定 prefetch 的次數 - HW 則無法無法有效控制,當 distance 較大時會消耗較多 bandwidth #### 3.2 Negative Impacts of Software Prefetching - Increased Instruction Count 指令數會增加 ![](https://i.imgur.com/mvkAh6j.png) - Static Insertion 在執行時期 (runtime)無法根據以下因素做動態調整 - varying memory latency - effective cache size - bandwidth - especially in heterogeneous architectures - Code Structure Change 原本 loop 的指令就少的時候,插入多的 prefetch 指令會導致結構改變 (distance 過短無法隱藏 latency) #### 3.3 Synergistic Effects when Using Software and Hardware Prefetching Together - Handling Multiple Streams - SW 和 HW 分工 - SW 處理 irregular stream - HW 處理 regular stream - Positive Training - SW 訓練 HW - 當 SW 太慢,可以由 HW 取代 #### 3.4 Antagonistic Effects when Using Software and Hardware Prefetching Together - Negative Training - SW 的 prefetch request 會降低 HW 的速度 - SW 的指令會引發 HW 的 prefetch,導致 early - Harmful Software Prefetching - 單用 SW 的 prefetch 較為精準 - 因為 HW 是由 SW 訓練的,當 SW 出問題會連帶影響 ## 開發紀錄 ### Original - 先理解程式碼, `Makefile`裡只有簡單的`$(CC) $(CFLAGS) -o main main.c` 所以 `make` 之後只產生 `main.c`,那我先執行看看 ``` sse prefetch: 59432 us sse: 116758 us naive: 242177 us ``` - 再回去看 `main.c` 發現有 `#include "impl.c"`,也就是以上三種的實作 code - 作業要求中有提到要學習 [你所不知道的 C 語言:物件導向程式設計篇](https://hackmd.io/s/HJLyQaQMl) 裡提到的封裝方法 - 為了實作物件導向並統一介面,因此先修改 `Makefile` 並分別產生執行檔 ```c= all: $(GIT_HOOKS) main.c $(CC) $(CFLAGS) -o main main.c // before vs. after EXEC = naive_transpose sse_transpose sse_prefetch_transpose naive_transpose: main.c $(CC) $(CFLAGS) -DNAIVE_TRANSPOSE -o $@ main.c sse_transpose: main.c $(CC) $(CFLAGS) -DSSE_TRANSPOSE -o $@ main.c sse_prefetch_transpose: main.c $(CC) $(CFLAGS) -DSSE_PREFETCH_TRANSPOSE -o $@ main.c all: $(GIT_HOOKS) $(EXEC) ``` > 這邊或許可以參考 [勃興學長]("https://github.com/0140454/compute-pi/commit/89ab12a144d0ce6d2d3c498e50277fe81c42b8d4")的 commit 用 % 取代 NAIVE_TRANSPOSE (不確定用法,只是突然想到),等之後改完後面的再回來補 - 以下是 `main.c` 修改的部分內容 ```c= typedef struct object Object; typedef void (*func_t)(int *src, int *dst, int w, int h); int init_sse_transpose(Object **self) { if ((*self = malloc(sizeof(Object))) == NULL) return -1; (*self) -> transpose = sse_transpose) return 0; } Object *o = NULL; #ifdef SSE_TRANSPOSE if (init_sse_transpose(&o) == -1) printf("error."); #endif ... o->transpose(src, out0, TEST_W, TEST_H); // change from // sse_prefetch_transpose(src, out0, TEST_W, TEST_H); ... #ifdef SSE_TRANSPOSE printf("SSE_TRANSPOSE: \t %ld us\n", diff_in_us(start, end)); #endif ``` - 接下來要使用 perf 分析效能 ``` perf stat --repeat (次數) -e cache-misses,cache-reference,instructions,cycles ./(執行檔) ``` ||cache-misses|cache-references|instructions|cycles| |:---:|:---:|:---:|:---:|:---:|:---:|:---:| |naive|90.836%|26,325,696|1,465,339,765|1,130,960,634| |sse|85.321 %|12,703,570|1,253,388,735|771,443,403| |sse_prefetch|80.887 %|9,710,287|1,299,456,469|625,649,736| > 這邊可以看到雖然 cache-miss 沒有下降很多,但時間卻大幅降低。這是因為就算是 cache hit,還是會有 L1, L2, L3 的時間差異,例如 L1 -> reg. = 1 cycle,但 L3 -> reg. = 10 cycle (論文中也提到 SW prefetch 較有機會放到 L1)。 :::danger 請從 latency 落差去解釋數據 --jserv ::: ### 實驗一、調整 prefetch distance - 在進行實驗前,先看懂 `impl.c` ```c= void naive_transpose(int *src, int *dst, int w, int h) { for (int x = 0; x < w; x++) for (int y = 0; y < h; y++) *(dst + x * h + y) = *(src + y * w + x); } ``` - 上面就是一般的轉置,接下來看 sse 的版本 ```c= void sse_transpose(int *src, int *dst, int w, int h) { for (int x = 0; x < w; x += 4) { for (int y = 0; y < h; y += 4) { __m128i I0 = _mm_loadu_si128((__m128i *)(src + (y + 0) * w + x)); __m128i I1 = _mm_loadu_si128((__m128i *)(src + (y + 1) * w + x)); __m128i I2 = _mm_loadu_si128((__m128i *)(src + (y + 2) * w + x)); __m128i I3 = _mm_loadu_si128((__m128i *)(src + (y + 3) * w + x)); __m128i T0 = _mm_unpacklo_epi32(I0, I1); __m128i T1 = _mm_unpacklo_epi32(I2, I3); __m128i T2 = _mm_unpackhi_epi32(I0, I1); __m128i T3 = _mm_unpackhi_epi32(I2, I3); I0 = _mm_unpacklo_epi64(T0, T1); I1 = _mm_unpackhi_epi64(T0, T1); I2 = _mm_unpacklo_epi64(T2, T3); I3 = _mm_unpackhi_epi64(T2, T3); _mm_storeu_si128((__m128i *)(dst + ((x + 0) * h) + y), I0); _mm_storeu_si128((__m128i *)(dst + ((x + 1) * h) + y), I1); _mm_storeu_si128((__m128i *)(dst + ((x + 2) * h) + y), I2); _mm_storeu_si128((__m128i *)(dst + ((x + 3) * h) + y), I3); } } } ``` - 這邊有兩個要理解的地方是,sse 怎麼運作,還有為什麼會比較快 - 運作說明: - `mm_loadu` 就是把一次把 128 bit 的資料放進 reg. - `__m128i T0 = _mm_unpacklo_epi32(I0, I1)` 意思是將 I0 和 I1 的 low 128 bit 以 32 bit 為單位交錯放進 T0 - `I0 = _mm_unpacklo_epi64(T0, T1)`就是處理 high 128 bit - 參考共筆:[ierosodin]("https://hackmd.io/s/rkX95E-il")、[carolc0708]("https://hackmd.io/s/B1lDZtO0#使用avx來提升效能") ![](https://i.imgur.com/0DJa6eV.png) - 速度差異: - 先來看以下一段簡易的程式碼 ```= int p[100][100]={} for (int i=0; i<100; ++i) for (int j=0; j<100; ++i) printf ("%d ", p[i][j]); // stream for (int i=0; i<100; ++i) for (int j=0; j<100; ++i) printf ("%d ", p[j][i]); // stride ``` - 如果拿去分析會發現 `p[j][i]` 較慢,因為 C 語言是 row major,跳著讀取會比較慢 (prefetch 機制) - 而使用 sse 會比較快的原因是, naive 的寫法我們一次只讀一個 int ,而 sse 一次讀 128 bit,也就是 4 個int,也不會發生跳 row 的問題 - 再來是 sse_prefetch 的版本 ```c= #define PFDIST 8 _mm_prefetch(src+(y + PFDIST + 0) *w + x, _MM_HINT_T1); _mm_prefetch(src+(y + PFDIST + 1) *w + x, _MM_HINT_T1); _mm_prefetch(src+(y + PFDIST + 2) *w + x, _MM_HINT_T1); _mm_prefetch(src+(y + PFDIST + 3) *w + x, _MM_HINT_T1); ``` - 這邊看到兩個可以實驗的地方, PFDIST 和 locality - 先查看一個 loop 會花多久時間 (s),再除 latency (l) - 假設這邊 D=8 是 timely 的話,預期結果是 8 > 4=12 > 0=16 - 以下是實驗結果 ``` D=0 -> 102078 us D=4 -> 68133 us D=8 -> 69905 us D=12-> 82102 us D=16-> 81566 us D=20-> 111937 us ``` - 可以發現 D 差不多是以 8 為一個級距,所以原先的預期錯誤 - 值得一提的是 D=0 還是比沒有 prefetch 的 sse 的版本快 > 這邊不太理解原因,可能要用其他工具分析 latency 才能說明 ### 實驗二、修改 locality hint - 修改 `_MM_HINT_T1` 為 `T2, T3, NTA`,預期結果是時間會越來越長 ``` T1 = 81408 us T2 = 107164 us T3 = 71822 us NTA= 86694 us ``` > 為什麼... ### 實驗三、新增 avx 和 avx prefetch 版 ```c= void avx_prefetch_transpose(int *src, int *dst, int w, int h) { for ( int x = 0; x < w; x += 8 ) { for ( int y = 0; y < h; y += 8 ) { #define PFDISTHUGE 16 _mm_prefetch(src+(y + PFDISTHUGE + 0) *w + x, _MM_HINT_T0); _mm_prefetch(src+(y + PFDISTHUGE + 1) *w + x, _MM_HINT_T0); _mm_prefetch(src+(y + PFDISTHUGE + 2) *w + x, _MM_HINT_T0); _mm_prefetch(src+(y + PFDISTHUGE + 3) *w + x, _MM_HINT_T0); _mm_prefetch(src+(y + PFDISTHUGE + 4) *w + x, _MM_HINT_T0); _mm_prefetch(src+(y + PFDISTHUGE + 5) *w + x, _MM_HINT_T0); _mm_prefetch(src+(y + PFDISTHUGE + 6) *w + x, _MM_HINT_T0); _mm_prefetch(src+(y + PFDISTHUGE + 7) *w + x, _MM_HINT_T0); __m256i I0, I1, I2, I3, I4, I5, I6, I7, T0, T1, T2, T3, T4, T5, T6, T7; I0 = _mm256_loadu_si256((__m256i *)(src + (y + 0) * w + x)); I1 = _mm256_loadu_si256((__m256i *)(src + (y + 1) * w + x)); I2 = _mm256_loadu_si256((__m256i *)(src + (y + 2) * w + x)); I3 = _mm256_loadu_si256((__m256i *)(src + (y + 3) * w + x)); I4 = _mm256_loadu_si256((__m256i *)(src + (y + 4) * w + x)); I5 = _mm256_loadu_si256((__m256i *)(src + (y + 5) * w + x)); I6 = _mm256_loadu_si256((__m256i *)(src + (y + 6) * w + x)); I7 = _mm256_loadu_si256((__m256i *)(src + (y + 7) * w + x)); T0 = _mm256_unpacklo_epi32(I0, I1); T1 = _mm256_unpackhi_epi32(I0, I1); T2 = _mm256_unpacklo_epi32(I2, I3); T3 = _mm256_unpackhi_epi32(I2, I3); T4 = _mm256_unpacklo_epi32(I4, I5); T5 = _mm256_unpackhi_epi32(I4, I5); T6 = _mm256_unpacklo_epi32(I6, I7); T7 = _mm256_unpackhi_epi32(I6, I7); I0 = _mm256_unpacklo_epi64(T0, T2); I1 = _mm256_unpackhi_epi64(T0, T2); I2 = _mm256_unpacklo_epi64(T1, T3); I3 = _mm256_unpackhi_epi64(T1, T3); I4 = _mm256_unpacklo_epi64(T4, T6); I5 = _mm256_unpackhi_epi64(T4, T6); I6 = _mm256_unpacklo_epi64(T5, T7); I7 = _mm256_unpackhi_epi64(T5, T7); T0 = _mm256_permute2x128_si256(I0, I4, 0x20); T1 = _mm256_permute2x128_si256(I1, I5, 0x20); T2 = _mm256_permute2x128_si256(I2, I6, 0x20); T3 = _mm256_permute2x128_si256(I3, I7, 0x20); T4 = _mm256_permute2x128_si256(I0, I4, 0x31); T5 = _mm256_permute2x128_si256(I1, I5, 0x31); T6 = _mm256_permute2x128_si256(I2, I6, 0x31); T7 = _mm256_permute2x128_si256(I3, I7, 0x31); _mm256_storeu_si256((__m256i *)(dst + ((x + 0) * h) + y), T0); _mm256_storeu_si256((__m256i *)(dst + ((x + 1) * h) + y), T1); _mm256_storeu_si256((__m256i *)(dst + ((x + 2) * h) + y), T2); _mm256_storeu_si256((__m256i *)(dst + ((x + 3) * h) + y), T3); _mm256_storeu_si256((__m256i *)(dst + ((x + 4) * h) + y), T4); _mm256_storeu_si256((__m256i *)(dst + ((x + 5) * h) + y), T5); _mm256_storeu_si256((__m256i *)(dst + ((x + 6) * h) + y), T6); _mm256_storeu_si256((__m256i *)(dst + ((x + 7) * h) + y), T7); } } } ``` - 由於 avx 是 256 bit (相對於 sse 是 128 bit),預期時間會是比一半再多一些 (Amdahl’s Law) - 五個版本的時間比分別是 40 : 14 : 13 : 8 : 11 - 後來想到因為 avx 做的是 8`*`8 的矩陣,所以應該會比 sse 做 4`*`4 的快不到兩倍