# 2017q1 Homework3 (software-pipelining) contributed by <`tina0405`> >>中英文字間請以空白隔開 >>[name=課程助教][color=red] >>好,已改。謝謝助教 [name=tina0405] ## 作業要求1: #### 論文筆記 ![](https://i.imgur.com/RP7RgKU.png) * About software and hardware prefetching: * 文中寫在改進軟體預取方面: * Positive 的表現增加 5%。 * Negative 的表現減少 5%。 (Q:但是我在這段看不出與和者做比較,5% 是全部的 5% 嗎?所以來看看 Table V) ![](https://i.imgur.com/0NTVEwe.png) * (TABLE V)只有軟體預取沒有硬體的情形下,各個 baseline 上的執行時間。 * 此篇論文也丟出3個問題: * 在軟體預取上什麼是它的限制和 overhead? * 在硬體預取上什麼是它的限制和 overhead? * 硬體的預取受限於 stream 和 stride 的預取。 * 應用軟體+硬體預取有什麼好處? ![](https://i.imgur.com/R2dJN2I.png) >> 在 steaming prefetch 和 GHB prefetch,intrinsics 上目前還沒看到明確的解釋 * TABLE.I說不只針對普通的資料結構還有特別的,例如:RDS(Recursive Data Structure)insert prefetch intrinsics。 * hashing 更難有效的預取。 * 很多軟體和硬體的預取目的都是在預測之後的 cache miss addresses ![](https://i.imgur.com/H2wnSSb.png) 註:Temporal — Data will be used again soon 參考[論文 When Prefetching Works, When It Doesn’t, and Why 重點提示和解說](https://hackmd.io/s/HJtfT3icx#) * Software Prefetch Intrinsics: * 例子:_mm_prefetch(char *ptr,int hint) * --> 需要有預取的地址及要用到的 hint (上圖) * 一個 intrinsic 大致上可以分成 2個指令(直接獲取的地址預取)或是4個指令(間接獲取的記憶體地址) >>關於這裡的直接和間接: 我的理解是 直接:直接得到(eg.A[i])的東西。 間接:透兩層以上值的傳遞(eg.B[A[i]])才得到。 * 預取分為六類: ![](https://i.imgur.com/UENcFhI.png) * Software Prefetch Distance: * 簡稱 D=[l/s] * l->prefetch latency,s->loop bady 的最短路徑。 * 對 cache miss 而言,D 有重大的影響: * 不幸的是,平均的 memory lantency 和 執行迭代迴圈的時間都一直在改變。 * 所以 D 要夠大,大到可以剛好不會遇到 lantency * 但如果 D 太大,預取的資料將會被移出有效的快取,導致以下缺點: 1. 原本的資料將不再被預取(導致2) 2. 很少有機會覆蓋 3. 綜合以上兩點,cache miss 增加(我們不樂見的) * Direct and Indirect Memory Indexing * direct memory indexing: * only one prefetch load instruction * 建議用硬體做,更容易預取。 -->因為記憶體的地址在 STREAM 和 STRIDE 上是有規律的 * indirect memory indexing: * 用軟體做相對硬體簡單 * 需要更多的 overhead 比起 direct >>想法: >>預取就像間接的索引,會有更多的 overhead,但我之前一直覺得有很多的 overheads 是應該會更慢嗎?可是忽略了其實預取解決了 latency 的問題,在L1->L2->L3->中找不到所發生的cache miss,再回到主記憶體去找的 latency 是比 overhead更久的。 * 軟體預取的好處(比起硬體): * 大量的stream * 當stream數超過硬體的容量(硬體缺點) * 軟體的預取插入時會要求獨立每一筆 stream ,但硬體卻只會覺得收到極大的資料量。 * 極短的stream * 硬體至少會花費兩個 cache misses 去偵測 stream/stride 的距離 * stream 太短沒有足夠的 cache misses 去做硬體的預取和載入有用的 cache block 。 * 不規則的記憶體拿取(軟體的優點) * 舉例:因為硬體是有規則的拿取,所以如果像 RDS 這樣的 indirection ,會需要非常複雜的結構。 * cache locality hint * 現今高效能的硬體依然是放置資料在lower level cache , 而軟體則是放在 L1 ,那如果硬體要從 L2->L1 就會發生 Latency 。 * loop bounds * 軟體再這邊可以贏硬體是因為多了一些像是 loop unrolling , software pipelining , branch instruction。(在名詞解釋中討論) * 軟體預取的負面衝擊 * 增加指令數: * 不像硬體,軟體的預取會消耗掉拿取和執行的頻寬還有機械的資源。拿例子來說圖中的bwave就因為軟體預取而增加100%。 ![](https://i.imgur.com/b3bXlYJ.png) * 關於靜態的插入 就是因為再軟體內沒有一套固定的機制(不像硬體),所以也沒有一種可以適應的改變,那些變化的 memory latency 或 快取的大小也都無從掌握,導致插入靜態參數的困難。 * 程式結構的改變 * 因為執行時增加的指令數,讓軟體預取變困難,有時候適當的改變結構是必要的,例如:拆迴圈。 >>本來在 indirection 的時,軟體的不規則是優點,但在insert 這裡,反而變成了缺點。 * 軟體和硬體合用以增加效用 * 剛剛有看到軟硬體面對 stream 的優缺點,所以這裡的作法是: * 硬體去負責有規律的stream * 軟體去負責沒規律的stream * 利用軟體去 training 硬體,當軟體預取慢時,就由硬體預取來改進時間。 >>在這邊我對 training 一詞有不太理解的地方,如果以之前影像處理所說的 training 就是跑過一次後記住那些特徵,但這邊難道也是硬體跑過後要去紀錄哪個指令特別快或慢,再去補足嗎? * 軟體和硬體合用卻反效果 * 這邊early request說5.2.1再說明,有點看不懂為什麼是缺點,等看到那再回來補: * 軟體預取的請求會減低硬體 training * 軟體預取的指令會觸發硬體預取->造成 early request * 實驗方法 * prefetch intrinsic insertion algorithm ![](https://i.imgur.com/BRI7wnf.png) * prefetch 候選人應符合以下條件: * L1 cache misses / 1000 個指令 (MPKI) > 0.05 1.k 是常數 2.L = average memory latency 3.IPCbench = profiled average IPC/one benchmack 4.Wloop 是迴圈平均每一圈的指令數 ## 名詞解釋 * [prefetch:](https://zh.wikipedia.org/zh-tw/CPU%E7%BC%93%E5%AD%98) 可以隨機性地在資料被用到之前就將其取入快取。這一技術稱為Prefetch。本質上,加載整個快取塊其實即是一種預取。 因為還是不是很理解,想拿實際例子看看: * [DDR(雙倍資料傳輸)預取原理](http://www.techbang.com/posts/17190-development-history-of-memory-ddr-and-gddr-difference) Prefetch(預取)技術,DDR、DDR2、DDR3的分別採用2bit、4bit、8bit預取技術,藉此得以讓時脈翻倍。預取簡單來說,就是在I/O控制器發出請求之前,預先準備好2bit、4bit、8bit的資料。可將其視為並行(Parallel)轉換為串行(Serial)。其轉換類似多條水管連接到某個裝置,若輸出的水管口徑相同,那麼水壓(速率)必定提升。 ![](https://i.imgur.com/b69ND5n.png) ~~~ 自己的理解p refetch: 在 I/O 發出時脈前,就先將他就先將所需的資料存入快取,當資料真正被需要時, 就可以很快地拿出來比起去 request from memory 快很多。 ~~~ * Software Prefetch Intrinsics: 文中解釋,是 X86 SSE SIMD延伸的一部份,後面的例子有舉出軟體預取的函式說明。(會在論文筆記中討論) * 參考 [迴圈展開](https://www.ptt.cc/bbs/C_and_CPP/M.1246071002.A.A54.html): * 文中的意思是希望減少判斷次數,因此將迴圈內的式子展開,例如: ~~~ clike= for ( i = 0; i < 100; ++i ) sum += a[ i ]; //展開成下列 for ( i = 0; i < 100; i += 5 ) { sum += a[ i ]; sum += a[ i + 1 ]; sum += a[ i + 2 ]; sum += a[ i + 3 ]; sum += a[ i + 4 ]; } ~~~ * 參考 [loop bound](http://dsp.ee.ncu.edu.tw/course/VDSP_99/lecture/ch2.pdf): * loop 的運算時間/延遲的迴圈數 ![](https://i.imgur.com/1Q9Qye9.png) * 參考 [branch instruction](http://ithelp.ithome.com.tw/articles/10158857) * 可以分成兩種: * 條件式 (Conditional Branch): 1.branch if equal (beq) 2.branch if not equal (bne) * 非條件式: 1.jump (j) * 參考 [pipeline1](https://zh.wikipedia.org/zh-tw/%E6%8C%87%E4%BB%A4%E7%AE%A1%E7%B7%9A%E5%8C%96),[pipeline2](http://blog.xuite.net/fishrabbit/BroadBand/4502419-Pipeline+Hazard) ![](https://i.imgur.com/bd9g3VY.png) 1.讀取指令 2.指令解碼與讀取暫存器 3.執行 4.記憶體存取 5.寫回暫存器 * 本來要一行一行讀的指令,現在經過 pipeline 降低CPU的閒置時間,進而提升效率 * [ 頻寬(Bandwidth)](http://blogger.gtwang.org/2013/12/network-lantency-and-bandwidth.html):傳輸媒介的最大吞吐量(throughput)。 >>如同上面所說的如果當 Bandwidth 就只有這麼大,那我軟體增加的指令數就會佔用有限的頻寬。 ## 作業要求2: # 開發環境 ~~~ tina@tina-X550VB:~$ lscpu Architecture: x86_64 CPU 作業模式: 32-bit, 64-bit Byte Order: Little Endian CPU(s): 4 On-line CPU(s) list: 0-3 每核心執行緒數:2 每通訊端核心數:2 Socket(s): 1 NUMA 節點: 1 供應商識別號: GenuineIntel CPU 家族: 6 型號: 58 Model name: Intel(R) Core(TM) i5-3230M CPU @ 2.60GHz 製程: 9 CPU MHz: 1270.750 CPU max MHz: 3200.0000 CPU min MHz: 1200.0000 BogoMIPS: 5188.47 虛擬: VT-x L1d 快取: 32K L1i 快取: 32K L2 快取: 256K L3 快取: 3072K NUMA node0 CPU(s): 0-3 ~~~ #### 1.執行 [prefetcher](https://github.com/sysprog21/prefetcher) ~~~ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 4 8 12 1 5 9 13 2 6 10 14 3 7 11 15 sse prefetch: 96069 us sse: 155224 us naive: 294317 us ~~~ #### 2.說明 `naive_transpose`, `sse_transpose`, `sse_prefetch_transpose` 之間的效能差異,以及 prefetcher 對 cache 的影響 先看一下定義:參考[Microsoft](https://technet.microsoft.com/en-us/library/x8atst9d(VS.90).aspx) Loads 128-bit value. ~~~ clike= __m128i _mm_load_si128 (__m128i *p); /*Return Value Returns the value loaded in a variable representing a register.*/ ~~~ Interleaves the lower 2 signed or unsigned 32-bit integers in a with the lower 2 signed or unsigned 32-bit integers in b. ~~~ clike= __m128i _mm_unpacklo_epi32 (__m128i a, __m128i b); /*Return Value r0 := a0 ; r1 := b0 r2 := a1 ; r3 := b1*/ ~~~ #### 原版的 transpose: ~~~ clike= 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 版本: * 我覺得以下版本利用了論文中的 loop unrolling 以減少判斷次數,減少 4 倍之多判斷次數,我認為這是他變快的原因。 ~~~ clike= 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 和 prefetch版本: ~~~ clike= void sse_prefetch_tranint w, int h) { for (int x = 0; x < w; x += 4) { for (int y = 0; y < h; y += 4) { #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); //...... } } } ~~~ * _mm_prefetch(char *ptr,int hint) * 需要有預取的地址及要用到的 hint * 我認為他在預取地址輸入 src+(y + PFDIST + 0) *w + x 是為了用最簡單方式但又不至於重複使用到地址的方法。 * 這裡使用 MM_HINT_T1 -> temporal data with respect to first level cache ![](https://i.imgur.com/H2wnSSb.png) * 快取的確有幫助 fetch 資料的速度,減少 lantency ,相對真的快很多,但我心裡想的是如果可以用 MM_HINT_T0 ,有 all level cache 可以幫助,那等等 fetch 資料時如果L1找不到再去L2->L3 找的速度應該還是比L1找不到往下找最後到 main memory 去找快很多吧! ### 測試(每組測三次): ~~~ MM_HINT_T0: sse prefetch: 90836 us sse: 157197 us naive: 335376 us sse prefetch: 92018 us sse: 151436 us naive: 317203 us sse prefetch: 98582 us sse: 161148 us naive: 355007 us MM_HINT_T1: sse prefetch: 88544 us sse: 134979 us naive: 282280 us sse prefetch: 83574 us sse: 138647 us naive: 277921 us sse prefetch: 83886 us sse: 140416 us naive: 281465 us MM_HINT_T2 sse prefetch: 118629 us sse: 177054 us naive: 354652 us sse prefetch: 114252 us sse: 178407 us naive: 363896 us sse prefetch: 130989 us sse: 194430 us naive: 380923 us ~~~ >>結果並不如預 MM_HINT_T1 (在 L1 的資料之後將會被使用)其實整體而言是花費最少時間的,我猜測應該是 MM_HINT_T2(在 L1,L2 的資料之後將會被使用),雖然多了空間去存放資料,但如果那些資源卡在那裡還未 access 或 release , 其等待時間將可能增加其他方面 latency。 ## 作業要求3: #### 在 github 上 fork [prefetcher](https://github.com/sysprog21/prefetcher),參照下方「見賢思齊:共筆選讀」的實驗,嘗試用 AVX 進一步提昇效能。分析的方法可參見 [Matrix Multiplication using SIMD](https://hackmd.io/s/Hk-llHEyx) #### 1.修改 `Makefile`,產生新的執行檔,分別對應於 `naive_transpose`, `sse_transpose`, `sse_prefetch_transpose`(學習 [phonebook](s/S1RVdgza) 的做法),學習 [你所不知道的 C 語言:物件導向程式設計篇](https://hackmd.io/s/HJLyQaQMl) 提到的封裝技巧,以物件導向的方式封裝轉置矩陣的不同實作,得以透過一致的介面 (interface) 存取個別方法並且評估效能 * 參考第一次作業[compute-pi](https://github.com/tina0405/compute-pi),和[makefile的撰寫規則](http://linux.org.tw/CLDP/OLD/doc/makefile-ch2.html) * 先把 makefile 分成三個執行檔`naive`, `sse_transpose`, `sse_prefetch_transpose` ~~~ clike= all: $(GIT_HOOKS) main.c $(CC) $(CFLAGS) main.c -DNAIVE -o naive $(CC) $(CFLAGS) main.c -DSSE -o sse $(CC) $(CFLAGS) main.c -DSSEPRE -o ssep ~~~ * 在 main.c 裡分組,需要用到的再 malloc 位置,還有記得要free() ,一開始就是因為忘記 free() ,一直程式記憶體區段錯誤。 ~~~ clike= struct timespec start, end; int *src = (int *) malloc(sizeof(int) * TEST_W * TEST_H); srand(time(NULL)); for (int y = 0; y < TEST_H; y++) for (int x = 0; x < TEST_W; x++) *(src + y * TEST_W + x) = rand(); #if defined(SSEPRE) int *out0 = (int *) malloc(sizeof(int) * TEST_W * TEST_H); clock_gettime(CLOCK_REALTIME, &start); sse_prefetch_transpose(src, out0, TEST_W, TEST_H); clock_gettime(CLOCK_REALTIME, &end); printf("sse prefetch: \t %ld us\n", diff_in_us(start, end)); free(out0); #endif #if defined(SSE) int *out1 = (int *) malloc(sizeof(int) * TEST_W * TEST_H); clock_gettime(CLOCK_REALTIME, &start); sse_transpose(src, out1, TEST_W, TEST_H); clock_gettime(CLOCK_REALTIME, &end); printf("sse: \t\t %ld us\n", diff_in_us(start, end)); free(out1); #endif #if defined(NAIVE) int *out2 = (int *) malloc(sizeof(int) * TEST_W * TEST_H); clock_gettime(CLOCK_REALTIME, &start); naive_transpose(src, out2, TEST_W, TEST_H); clock_gettime(CLOCK_REALTIME, &end); printf("naive: \t\t %ld us\n", diff_in_us(start, end)); free(out2); #endif free(src); ~~~ * ### Makefile的一般結構: target:dependency command * target(目標):一個目標檔,可以是Object檔,也可以是執行檔。還可以是一個標籤(Label)。 * dependency (依賴):要產生目標檔(target)所依賴哪些檔。 * command(命令): 建立專案時需要執行的shell命令 * 注意: command部分的每行的縮進必須要使用Tab鍵,就算用空白推到相應位置也不行,會出現紅色警示。 ![](https://i.imgur.com/bMsD9Dt.png) 最後要記得執行檔也要clean: * RM:刪除檔命令 ~~~ clike= Clean: $(RM) naive sse ssep ~~~ 想說改成下面:(直接跑結果) ~~~ clike= sse_transpose: main.c $(CC) main.c -DSSE -o sse ./sse sse_prefetch_transpose: main.c $(CC) $(CFLAGS) main.c -DSSEPRE -o ssep ./ssep naive_transpose: main.c $(CC) $(CFLAGS) main.c -DNAIVE -o naive ./naive ~~~ * #### 2.用 perf 分析 cache miss/hit #### 3.學習 `perf stat` 的 raw counter 命令 #### 4.參考 [Performance of SSE and AVX Instruction Sets](http://arxiv.org/pdf/1211.0820.pdf),用 SSE/AVX intrinsic 來改寫程式碼