# 2017q3 Homework2 (software-pipelining)
###### tags: `sysprogram`
contributed by <`st9007a`>
## [Github](https://github.com/st9007a/prefetcher)
## 開發環境
```
Linux 4.4.0-92-generic
Ubuntu 16.04.1 LTS
CPU family: 6
CPU model: 94
CPU model name: Intel(R) Core(TM) i5-6500 CPU @ 3.20GHz
L1d cache: 32K
L1i cache: 32K
L2 cache: 256K
L3 cache: 6144K
```
## 論文閱讀整理
- [原始文章](https://www.cc.gatech.edu/~hyesoon/lee_taco12.pdf)
### Section 1 - Introduction
- 有許多種 SW , HW prefetching 機制,卻只有相對簡單的 SW prefetching 演算法出現在 complier 中 (icc , gcc)
- 通常需要手動加入 prefetching intrinsics 來進行效能優化
- 兩個原因:
- 加入 prefetching intrinsics 的方式沒有一個嚴格的標準
- SW , HW prefetching 之間的交互影響複雜不易理解
- prefetcher 在不同 benchmark 中的比較如下圖:
- **SW+HW prefetcher 的最好執行結果**與 **HW prefetcher 最好執行結果** 進行比較,分為三個 group
![](https://i.imgur.com/WZBq0YI.png)
### Section 2 - Background of SW and HW prefetching
- 名詞定義:
- streams:unit-stride cache-line accesses
- strided:access stride distances greater than two cache lines
- 不同資料結構對應不同 prefetching 策略
![](https://i.imgur.com/2MaEONx.png)
#### Software Prefetch Intrinsics
- 使用 x86 SSE SIMD extenssions
- `void _mm_prefetch(char *ptr, int hint)`
- `ptr`:prefetch address
- `hint`:prefetch hint,參照下圖
![](https://i.imgur.com/NYzUgPW.png)
- intrinsics translate to instructions
- direct address prefetches:2 instructions
- indirect memory address prefetches:4 instructions
![](https://i.imgur.com/AQIv5JJ.png)
#### Prefetch Classification
- 分為六個類別
![](https://i.imgur.com/s16EPxL.png)
- Timely, Late, Early 在時間線上的定義
![](https://i.imgur.com/yQw0KXe.png)
#### Software Prefetch Distance
- 定義:在 array 的 index 中加入一常數 D 來代表要 prefetch 的 address,D 即是 prefetch distance
- e.g. 參考以下程式碼
```clike
int vector[100] = get_orgin(100);
for (int i; i < 100; i++) {
_mm_prefetch(&vector[i + 4], T0); //prefetch distance = 4
printf("%d\n", vector[i]);
}
```
- stream pattern 的 array structure 的 prefetch distance 可以表示成:$D > \lceil \frac{l}{s} \rceil$
- $l$:prefetch latency
- $s$:the length of the shortest path through the loop body
- prefetch distance 必須夠大到足以隱藏 memory latency
- prefetch distance 太小:prefetch request 太晚,cpu 已經開始在 cache 上存取資料
- prefetch distance 太大:prefetch address 太早出現在 cache 上,導致 prefetch address 可能被其他資料覆蓋,造成更多的 cache miss
#### Direct and Indirect Memory Indexing
- HW prefetcher 可以較輕易的 prefetch direct memory indexing 的資料
- 因為 memory address 是正常的 stream/stride 行為
- 相對地,HW prefetcher 不擅長處理 indirect memory indexing
- SW prefetcher 在處理 indirect memory indexing 比 HW prefetcher 還要有效率
- Indirect prefetch requests have a higher overhead
- 4 個 instructions , 2 次 address 載入
- 2 次 address 載入有相依性,無法並行處理
### Section 3 - Positive and Negative Impacts of Software Prefetching
#### SW prefetching 優點
- Large Number of Streams (Limited Hardware Resources)
- stream prefetcher( 其中一種 HW prefetcher ) 的 streams 數量受限於硬體資源
- SW prefetching 可以在每個 streams 加入獨立的 prefetch requests
- Short Streams
- HW prefetcher 至少需要 2 個 cache miss 來偵測 stream/stride 的 direction 跟 distance
- stream 過短的狀況下,沒有足夠的 cache miss 能訓練 HW prefetcher,進而載入有效的 cache block
- Irregular Memory Access
- 參照 Section 2 Direct and Indirect Memory Indexing
- SW prefetching 可以透過加入適當的 intrinsics 來存取 irregular data structure
- Cache Locality Hint
- HW prefetcher 將 prefetched data 放置於 low-level cache ( L2, L3 ),減 L1 cache pollution,但是 L2 到 L1 的延遲會影響效能
- SW prefetching 可以更靈活地將 prefetched data 放置到正確的 cache 上,參照 Section 2 Software Prefetch Intrinsics
- Loop Bounds
- SW prefetching 可以透過需多方法避免 prefetch requet out of array bound 的狀況,e.g. loop unrolling , software piplining , branch instructions
- It is not possible for HW prefetcher
#### SW prefetching 缺點
- Increased Instruction Count
- SW prefetching 需要消耗額外的機器資源來執行 prefetch 相關的 instruction,下圖為對應由於 SW prefetches 而增加的 instructions 量
![](https://i.imgur.com/ur7dAvr.png)
- Static Insertion
- SW prefetching 策略在編譯完之後就無法更動,因此 SW prefetching 無法即時的應付多種 memory latency, cache size, bandwidth
- HW prefetcher 可以適時的修正 prefetch distance 跟 degree
- Code Structure Change
- 在每次 loop 要執行的 instructions 極少的狀況下,SW prefetching 無法隱藏住 memory latency
- 在某些狀況下,加入 prefetch 會導致 instructions 增加過多,使結構調整變得困難
#### SW + HW prefetching 的優點
- Handling Multiple Streams
- 兩種 prefetching 機制並用可以處理更多 data streams
- e.g. HW prefetching 處理常規資料,SW prefetching 處理非常規資料
- Positive Training
- SW prefetch requests 可以幫助訓練 HW prefetcher
#### SW + HW prefetching 的缺點
- Negative Training
- SW prefetching 會使 HW prefetcher 訓練速度變慢
- 如果 SW prefetching 隱藏一到多個 data streams 會使 HW prefetcher 無法確實的訓練
- SW prefetching 可能觸發 HW prefetcher,造成 prefetch request 過早
- Harmful Software Prefetching
- HW prefetcher 精確度比 SW prefetching 還低,當 SW prefetching 出錯時會造成 HW prefetcher 精確度更低
### Section 4 - Experimental Methodology
#### Prefetch Intrinsic Insertion Algorithm
- 針對每個 prefetch candidate 加入 SW prefetch
- Prefetch candidate:每 1000 個 instructions 中 L1 misses 超過 5%
- Prefetch distance:$Distance = \frac{K \times L \times IPC_{bench}}{W_{loop}}$
- $K$:constant factor
- $L$:average memory latency
- $IPC_{bench}$:the profiled average IPC of each benchmark
- $W_{loop}$:s the average instruction count in one loop iteration
- 實驗使用 $K = 4 , L = 300$,$W_{loop} , IPC_{bench}$ 則由各 benchmark 的 profile data 決定
#### Simulation Methodology
- 使用 MacSim:a trace-driven cycle-level simulator,下圖為處理器的配置
![](https://i.imgur.com/iQgjbbk.png)
- Perform experiments on 13 memory-intensive benchmarks from SPEC CPU 2006
- Excluded four additional benchmarks that meet this definition of memory intensive — omnetpp, astar, xalancbmk, and wrf — because we were not able to insert any software prefetch intrinsics.
- 下圖為 evaluated benchmark 在 baesline processor 的特徵
![](https://i.imgur.com/xkNv9OS.png)
### Section 5 - Evaluation:Base Obvervations of Prefetching
#### Overhead and Limitations of Software Prefetching
- Instruction Overhead
- 參照 Section 3 SW prefetching 缺點
- 除了 prefetch instruction 以外,還增加了 indirect memory access 跟 index calculation 相關的 instructions
- GemsFDTD 增加了超過 50% 的 instructions,但是 prefetch 的效益還是大於 instructions 數量增加的影響
- Software Prefetching Overhead
- 針對個別去除 overhead of cache pollution , bandwidth consumption , memory access latency , redundant prefetch , instruction 做量測,結果如下圖
![](https://i.imgur.com/McSNKJN.png)
- Y 軸為去除 overhead 後所減少的執行 cycle 數量
- cache pollution 原因在於錯誤的 prefetch,所以其 overhead 影響不大
- bandwidth comsumption 影響也不大,這證明了機器提供了足夠的 bandwidth
- memory latency access 的影響較大,代表 SW prefetching 無法完全隱藏 memory latency
- redundant prefetch 的值較小,這代表即使實驗中 redundant prefetch 數量很大,其影響仍然能忽略不計
- 雖然在 GemsFDTD , bwaves , leslie3d 增加了許多 instructions (參照 Section 3 SW prefetching 缺點),其 ovrehead of instructions 仍然不大
#### The Effect of Prefetch Distance
- 下圖為 prefetch distance 效能的靈敏度
- X 軸為相對於 base prefetch distance 的 prefetch distance
- 參照Section 2 Prefetch Distance,base prefetch distance 套用公式 $D > \lceil \frac{l}{s} \rceil$
- X 軸的一個單位長度為一個 cache block
![](https://i.imgur.com/GtyEH1V.png)
- 根據 the optimal zone 以及 performance variance 將所有 benchmark 分為 5 個 groups,如下圖
![](https://i.imgur.com/OH1dt6g.png)
#### Static Distance vs. Machine Configuration
- 比較三種不同的機器配置(base, less-aggressive, aggressive,參照 Section 4 Simulation Methodology)
![](https://i.imgur.com/xggze8p.png)
- 左邊三張圖為在不同機器配置下的 best distance
- 右邊三張圖 baseline 的 best distance 與不同機器配置的 best distance 之間的 performance difference
- 除了 lbm 以外,其他的 benchmark 在不同機器配置下效能差異並不大
- 結論:static prefetch distance variance 在不同機器配置下並不會影響效能太多
#### Cache-Level Insertion Policy
- 參照 Section 3 SW prefetching 優點 - Cache Locality Hint
- 下圖為不同 cache locality hint 對應不同 benchmark 比較結果
![](https://i.imgur.com/6gF3Yqw.png)
- T0 將 prefetched data 放置到 L1 cache,因此使 L1 cache miss rate 下降
- 在 benchmark 的 libquantum 就有這個現象,其 L1 cache miss rate 從 10.4% 降至 0.01%
#### Effect of Using Hardware and Software Prefetching Together
- Hardware Prefetcher Training Effects
- two different hardware prefetcher training policies
- NT (SW+GHB, SW+STR):HW prefetcher 訓練演算法忽略 SW prefetch request,僅透過 cache miss 來訓練
- Train (SW+GHB+T, SW+STR+T):HW prefetcher 訓練演算法透過 SW prefetch request 以及 cache miss 來訓練
- 下圖為利用 SW prefetching 訓練 HW prefetcher 的效能比較
![](https://i.imgur.com/RQtYTlT.png)
- 透過對 HW prefetching 預先做訓練可以達到 3% - 5% 的效能成長
- 然而效能下降幅度也很大 (milc , libquantum)
- 結論:負面影響非常顯著,因此透過 SW prefetching 來訓練 HW prefetching 不是一個好方法
#### Prefetch Coverage
- 定義:the ratio of useful prefetches to total L2 misses
![](https://i.imgur.com/Ko4hM0O.png)
- 理論上,SW prefetching 的 coverage 要高於 HW prefetchers,但是在 neutral 與 negative group 中,SW prefetching 的 coverage都小於 HW prefetchers
- 結論:less coverage is the main reason for performance loss in the neutral and negative groups
#### Prefetching Classification
- 參照 Section 2 Prefetch Classification
- 下圖為不同 prefetch requests 在各個 benchmark 以及不同 prefetchers 中所佔的比例
![](https://i.imgur.com/cMWpR6A.png)
#### Using the GCC Compiler
- 之前的實驗使用 icc compiler
- gcc compiler 不支援 Fortran 的 intrinsics
- 實驗進行方式為 gcc 編譯完程式碼後再加入 intrinisics,並且排除 Fortran benchmarks
- gcc version = 4.4
- 實驗結果如下
![](https://i.imgur.com/pQWSHvy.png)
- 下圖為 gcc compiler 增加的 instructions 數量
![](https://i.imgur.com/4iGvixK.png)
- The gcc compiler inserts more software prefetches than icc, except for mcf and lbm.
#### Real System Experiments
- 在兩個 Intel 處理器中量測 SW prefetching 的效果
- Core 2
- Nehalem
![](https://i.imgur.com/QaDue61.png)
- 在實驗中使用完整的 reference input sets,而不是只有 simpointed section
- 下圖表示 simulated region 與 entire execution 有相似的 prefetch instructions
![](https://i.imgur.com/izR9nDR.png)
- 下圖為實驗結果
![](https://i.imgur.com/VlIZfTe.png)
- 在 real system 中 cactusADM, soplex, bwaves, sphinx3, leslie3d 效能下降,儘管它們在 simulater 中效能是上升的
- all benchmarks in the positive group also show benefits from using software prefetches
- Nehalem 的 HW prefetcher 效果較 Core 2 好
- 由於 Nehalem 的 HW prefetcher 有 the multiple cache levels,因此 SW prefetching 的效益有所下降
#### Profile-Guided Optimization (PGO)
- perform experiments using both icc and gcc, but we do not include the results from icc because it does not insert any software prefetches
- use gcc 4.4 with `fprofile-generate` and `fprofile-use` flags
- 下圖為實驗結果
![](https://i.imgur.com/PbHXQbo.png)
- 大部分的 PGO 效能表現介於 base 與 SW 之間
- 執行效能的效益並非全部來自於 SW prefetching
- PGO 減少了 total instruction count 以及 prefetch instruction count
#### Hardware Prefetcher for Short Streams
- short stream 是 HW prefetcher 的弱點 (參照 Section 3 SW prefetching 優點 - short stream)
- ASD hardware prefetcher:一個可用於處理 short stream 的 HW prefetcher(僅概念,沒有被實作出來)
- 下圖為 ASD 與 SW/HW prefetching 的實驗結果
![](https://i.imgur.com/vXKWmaq.png)
- milc 有 short stream 的行為,而 ASD 在沒有 SW prefetching 的狀況下有 7% 的效能提升
- SW prefetching 本身就能讓執行速度快 2 倍以上
- ASD 的效益比 STR 與 GHB 好,但是效益 < 3%
- gcc, sph, les 在結合 ASD 與 SW prefetching 以及 GHB/STR HW prefetching 的狀況下,效能有所提升,但是提升幅度 < 2%
- 結論:在處理 short stream 的情況下,SW prefetching 的效率大於 ASD
#### Content Directed Prefetching
- 處理 linked 或 irregular 的 data structure
- 下圖為實驗結果,只處理 gcc 與 mcf 這兩個 benchmark,因為只有這兩個有受影養
![](https://i.imgur.com/4PzQ4mX.png)
- CDF HW prefetcher 對 gcc , mcf 分別提升了 6.5% 與 3.9%
- 對 CDF 進行防止 cache pollution 的處理後效能又個別上升了 5% 與 2%
- SW prefetching 效能提升 78%
- 結論:SW prefetching 在處理 irregular data structure 上是比較有效率的
#### Manual Tuning
- 無法確定 SW prefetching 將結果優化到最好
- 調整項目:
- remove useless prefetches
- adjust prefetch distance
- apply additional overhead-reduction techniques (loop unrolling , prologue/epilogue transformations)
- 事先排除 milc, GemsFDTD, lbm, bwaves, bzip2,因為它們不是已經最佳化了,就是需要大幅改變程式碼結構
- 下圖為實驗結果
![](https://i.imgur.com/Ggm8vgJ.png)
- speedup 在 neutral 與 negative group 中比 positive group 高,代表手動移除 negative intrinsics 是一個有效的方法
- 在 positive group 中,manual tuning 與 Section 4 的演算法彼此間的效能沒有顯著的改變,這代表演算法就已經是一個很好的 guideline for inserting software prefetch intrinisics.
#### Summary
- 在 regular access pattern 的狀況下,HW prefetchers 會 under-exploit,尤其是 short stream,這時使用 SW prefetching 的效率是比較好的
- Prefetch distance 不會受限於機器配置,只要 prefetch distance 大於公式的最小距離,大部分的 applications 不會因為 prefetch distance 而受影響
- 雖然 out-of-order execution 可以容忍 L1 cache misses,但是當 L1 cache misses 超過 20% 時,利用 prefetch 來降低 L1 cache misses 是比較有效率的
- The overhead of useless prefetching instructions is not very significant.
- SW prefetching 可以訓練 HW prefetcher 來提升效能,但是也可能拖垮效能
### Section 6 - Case Studies:Source Code Analysis of Individual Benchmarks
**參照原文 Section 6**
### Section 7 - Relatived Work
- Reducing software prefetching overhead
- Hardware and software cooperative prefetching
### Section 8 - Conclusion and Furture Work
#### Conclusion
- 選擇一個 static prefetch distance 是 aggressive SW prefetching 的限制
- 從實驗中可以看到 aggressive SW prefetching 產生了許多 early prefetch requests,optimal prefetch distance 也會受機器配置影響
- 但是從實驗中可以觀察到,在大部分的 benchmark 中,prefetch distance 不容易影響效能,也很容易調整
- SW prefetch 有高 coverage of delinquent loads,可以透過 prefetch 來減少額外 instruction 的 overhead
- Prefetching for short streams shows the most positive effects.
- HW prefetchers 沒辦法 prefetch short stream, irregular data structure, a large number of concurrent streams
- 必須使用 software-base prefetching techniques
- Coverage 可以做為選擇 prefetch scheme 的主要評估
- 透過 coverage 可以得知應該使用 SW prefetching 來處理 short stream 以及 indirect reference
- 而在 strong stream 或 regular stride pattern 中使用 SW prefetching 則對效能不會有太大的影響,甚至可能使 HW prefetching 效能下降
#### Futrue Work
- 開發 SW prefetch 演算法用於:
- 訓練 HW prefetchers
- 減少 SW/HW prefetching scheme 的 negative instructions 讓 compiler 可以更積極(?)的安插 prefetch instructions
## 編譯執行 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: 54226 us
sse: 135799 us
naive: 238329 us
```
上面兩個矩陣是用來驗證 $4\times4$ 矩陣的 transpose,而驗證時使用的方法是 `sse_transpose`
接著下面三個是使用三種不同方法來進行矩陣 transpose 時的執行時間,而這裡使用的矩陣大小為 $4096\times4096$,可以從輸出的結果知道效能是 sse prefetch > sse > naive,其中 sse 使用的是 intrinsics function
再來看看這三個方法是怎麼時做的,首先是 `naive_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);
}
```
`naive_transpose()` 將 `src` 的 (x, y) 位置的值直接 assign 到 `dst` 的 (y, x) 位置
然後是 `sse_transpose()`
```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);
}
}
}
```
根據 [intel intrinsics guide](https://software.intel.com/sites/landingpage/IntrinsicsGuide/#techs=SSE,SSE2,SSE3,SSE4_1,SSE4_2&expand=3162),可以知道程式碼中每個 intrinsics function 的作用
- `__m128i __mm_loadu_si128(__m128i const *mem_addr)`:將 128-bit 的整數從 mem_addr 載入到 dst,dst 會作為回傳值回傳出來
- `__m128i __mm_unpacklo_epi32(__m128i a, __m128i b)`:將 a 與 b 低位的 64-bit 資料切成 a[31:0] , a[63:32] , b[31:0] , b[63:32],然後填入 dst 回傳,dst 由高位到低位依序是 b[63:32] , a[63:32] , b[31:0] , a[31:0]
- `__mm128i __mm_unpackhi_epi32(__m128i a, __m128i b)`:行為與 `__mm_unpacklo_epi32` 類似,只是改成取 a 與 b 的高位
- `__m128i _mm_unpacklo_epi64 (__m128i a, __m128i b)`: 行為與 `__mm_unpacklo_epi32` 類似,只是資料不會被分割,所以 dst 由高位到低位是 b[63:0], a[63:0]
- `__m128i _mm_unpackhi_epi64 (__m128i a, __m128i b)`:行為與 `__mm_unpackhi_epi32` 類似,dst 由高位到低位是 b[127:64], a[127:64]
- `void __mm_storeu_si128(__m128i *mem_addr, __m128i a)`:將 a 的值寫入 mem_addr 中
知道了每個 function 的功用後,sse_transpose 做了什麼還是很難想像,所以先帶入 $4\times4$ 矩陣觀察其行為,選擇 $4\times4$ 的原因是 for loop 只需做一次就行,方便觀察
```
src_matrix = {
{ 1, 2, 3, 4},
{ 5, 6, 7, 8},
{ 9,10,11,12},
{13,14,15,16},
}
I0 = { 1, 2, 3, 4}
I1 = { 5, 6, 7, 8}
I2 = { 9,10,11,12}
I3 = {13,14,15,16}
T0 = { 1, 5, 2, 6}
T1 = { 9,13,10,14}
T2 = { 3, 7, 4, 8}
T3 = {11,15,12,16}
I0 = { 1, 5, 9,13}
I1 = { 2, 6,10,14}
I2 = { 3, 7,11,15}
I3 = { 4, 8,12,16}
dst_matrix = {
{ 1, 5, 9,13},
{ 2, 6,10,14},
{ 3, 7,11,15},
{ 4, 8,12,16}
}
```
將每一行執行結果列出來後就可以看到,透過 unpack 的方式,矩陣被轉置了
最後是 `sse_prefetch_transpose()`
```clike
void sse_prefetch_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) {
#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);
__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_transpose()` 除了 prefetch 的部分,其他部分跟 `sse_transpose()` 是一樣的,而 prefetch 的部分則是 prefetch 了 往後數 PFDIST + 0 ~ 3 個 row 的資料並且放置到 L2 以及更高階的 cache 上
## 分析效能
為了能個別測試效能,所以先設計一個通用的介面 `transpose.h`
```clike
void transpose(int *src, int *dst, int w, int h);
```
接著用不同檔案來個別實作不同方法:`naive_transpose.c` , `sse_transpose.c` , `sse_prefetch_transpose.c`
完成後,修改一下 `main.c` 跟 `Makefile` 讓其能自動產生對應的執行檔,接著用 `perf stat` 來看看各自的 cache miss
```
$ perf stat --repeat 100 -e cache-misses,cache-references,instructions,cycles ./naive_transpose
Performance counter stats for './navie_transpose' (100 runs):
4108,3426 cache-misses # 86.021 % of all cache refs ( +- 0.31% )
4775,9788 cache-references ( +- 0.28% )
14,4866,1674 instructions # 1.48 insns per cycle ( +- 0.00% )
9,7703,3562 cycles ( +- 0.07% )
0.343869644 seconds time elapsed ( +- 0.07% )
$ perf stat --repeat 100 -e cache-misses,cache-references,instructions,cycles ./sse_transpose
Performance counter stats for './sse_transpose' (100 runs):
1455,1286 cache-misses # 75.665 % of all cache refs ( +- 0.06% )
1923,1186 cache-references ( +- 0.07% )
12,3676,0551 instructions # 1.72 insns per cycle ( +- 0.00% )
7,1766,9516 cycles ( +- 0.07% )
0.264611085 seconds time elapsed ( +- 0.17% )
$ perf stat --repeat 100 -e cache-misses,cache-references,instructions,cycles ./sse_prefetch_transpose
Performance counter stats for './sse_prefetch_transpose' (100 runs):
823,1685 cache-misses # 64.939 % of all cache refs ( +- 0.18% )
1267,5942 cache-references ( +- 0.12% )
12,8279,3332 instructions # 2.30 insns per cycle ( +- 0.00% )
5,5755,6001 cycles ( +- 0.01% )
0.156492681 seconds time elapsed ( +- 0.05% )
```
可以看到 prefetch 讓 cache miss 下降了,接著搭配 raw counter 使用 perf stat
raw counter 參照 [Intel® 64 and IA-32 Architectures Developer’s Manual: Vol. 3B](https://www.intel.com/content/www/us/en/architecture-and-technology/64-ia-32-architectures-software-developer-vol-3b-part-2-manual.html) 的第 19-2 章:Performance Monitoring Events For 6th Generation Intel®Core™ Processor And 7th Generation Intel®Core™ Processor
- r2124:L2_RQSTS.DEMAND_DATA_RD_MISS (Demand Data Read requests that missed L2, no rejects.)
- r4124:L2_RQSTS.DEMAND_DATA_RD_HIT (Demand Data Read requests that hit L2 cache.)
- rE124:L2_RQSTS.ALL_DEMAND_DATA_RD (All demand data read requests to L2.)
找了一下,決定觀察這三個 event,原因在於上面 prefetch hint 為 T1,T1 會將 prefetched data 放置於 L2 以及更高階的 cache,所以理論上可以在 L2 cache 上觀察到明顯的數據變化,第三個 event 是拿來算百分比用的
```
$ perf stat --repeat 100 -e r2124,r4124,rE124 ./navie_transpose
Performance counter stats for './navie_transpose' (100 runs):
1678,1605 r2124 ( +- 0.00% )
4,6208 r4124 ( +- 1.15% )
1683,4637 rE124 ( +- 0.00% )
0.343165430 seconds time elapsed ( +- 0.08% )
$ perf stat --repeat 100 -e r2124,r4124,rE124 ./sse_transpose
Performance counter stats for './sse_transpose' (100 runs):
421,6532 r2124 ( +- 0.00% )
2,3383 r4124 ( +- 0.39% )
424,9238 rE124 ( +- 0.01% )
0.264649831 seconds time elapsed ( +- 0.15% )
$ perf stat --repeat 100 -e r2124,r4124,rE124 ./sse_prefetch_transpose
Performance counter stats for './sse_prefetch_transpose' (100 runs):
3,3902 r2124 ( +- 0.30% )
332,5174 r4124 ( +- 0.29% )
336,6697 rE124 ( +- 0.29% )
0.156584539 seconds time elapsed ( +- 0.05% )
```
| | naive | sse | sse prefetch |
| -------- | -------- | -------- | ----|
| L2_RQSTS.DEMAND_DATA_RD_MISS | 1678,1605 <br/> (99.684%) | 421,6532 <br/> (99.699%) | 3,3902 <br/> (1.007%) |
| L2_RQSTS.DEMAND_DATA_RD_HIT | 4,6208 <br/> (0.274%) | 2,3383 <br/> (0.553%) | 332,5174 <br/> (98.767%) |
| L2_RQSTS.ALL_DEMAND_DATA_RD | 1683,4637 | 424,9238 | 336,6697 |
從上面的表格可以看到,naive 到 sse 的 L2 cache miss/hit rate 沒有顯著變化,但是 sse prefetch 很明顯的使 L2 hit rate 大幅提升,驗證了 prefetch hint = T1 的狀況 (註:這裡的 miss/hit rate 僅計算 demand data read request)
## AVX Transpose
AVX transpose 參考了 [stackoverflow -- Transpose an 8x8 float using AVX/AVX2](https://stackoverflow.com/questions/25622745/transpose-an-8x8-float-using-avx-avx2) 的解答,由於 AVX 的暫存器是 256-bit,一次可以讀取 8 個 32-bit 整數,所以 for loop 的 step 改成 8
```clike
for (int x = 0; x < w; x += 8) {
for (int y = 0; y < h; y += 8) {
__m256i I0 = _mm256_loadu_si256((__m256i *)(src + (y + 0) * w + x));
__m256i I1 = _mm256_loadu_si256((__m256i *)(src + (y + 1) * w + x));
__m256i I2 = _mm256_loadu_si256((__m256i *)(src + (y + 2) * w + x));
__m256i I3 = _mm256_loadu_si256((__m256i *)(src + (y + 3) * w + x));
__m256i I4 = _mm256_loadu_si256((__m256i *)(src + (y + 4) * w + x));
__m256i I5 = _mm256_loadu_si256((__m256i *)(src + (y + 5) * w + x));
__m256i I6 = _mm256_loadu_si256((__m256i *)(src + (y + 6) * w + x));
__m256i I7 = _mm256_loadu_si256((__m256i *)(src + (y + 7) * w + x));
__m256i T0 = _mm256_unpacklo_epi32(I0, I1);
__m256i T1 = _mm256_unpackhi_epi32(I0, I1);
__m256i T2 = _mm256_unpacklo_epi32(I2, I3);
__m256i T3 = _mm256_unpackhi_epi32(I2, I3);
__m256i T4 = _mm256_unpacklo_epi32(I4, I5);
__m256i T5 = _mm256_unpackhi_epi32(I4, I5);
__m256i T6 = _mm256_unpacklo_epi32(I6, I7);
__m256i T7 = _mm256_unpackhi_epi32(I6, I7);
I0 = (__m256i)_mm256_shuffle_ps(T0, T2, _MM_SHUFFLE(1, 0, 1, 0));
I1 = (__m256i)_mm256_shuffle_ps(T0, T2, _MM_SHUFFLE(3, 2, 3, 2));
I2 = (__m256i)_mm256_shuffle_ps(T1, T3, _MM_SHUFFLE(1, 0, 1, 0));
I3 = (__m256i)_mm256_shuffle_ps(T1, T3, _MM_SHUFFLE(3, 2, 3, 2));
I4 = (__m256i)_mm256_shuffle_ps(T4, T6, _MM_SHUFFLE(1, 0, 1, 0));
I5 = (__m256i)_mm256_shuffle_ps(T4, T6, _MM_SHUFFLE(3, 2, 3, 2));
I6 = (__m256i)_mm256_shuffle_ps(T5, T7, _MM_SHUFFLE(1, 0, 1, 0));
I7 = (__m256i)_mm256_shuffle_ps(T5, T7, _MM_SHUFFLE(3, 2, 3, 2));
T0 = _mm256_permute2f128_si256(I0, I4, 0x20);
T1 = _mm256_permute2f128_si256(I1, I5, 0x20);
T2 = _mm256_permute2f128_si256(I2, I6, 0x20);
T3 = _mm256_permute2f128_si256(I3, I7, 0x20);
T4 = _mm256_permute2f128_si256(I0, I4, 0x31);
T5 = _mm256_permute2f128_si256(I1, I5, 0x31);
T6 = _mm256_permute2f128_si256(I2, I6, 0x31);
T7 = _mm256_permute2f128_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);
}
}
```
附上執行結果比較,`avx_prefetch_transpose` 的 prefetch distance 這裡事先設定為 8
```
exec naive_transpose
execution time: 225405 us
exec sse_transpose
execution time: 151882 us
exec sse_prefetch_transpose
execution time: 43714 us
exec avx_transpose
execution time: 56837 us
exec avx_prefetch_transpose
execution time: 43255 us
```
可以看到 `avx_transpose` 的執行速度比 `sse_transpose` 還快,執行速度的部分可以參照 [Performance of SSE and AVX Instruction Sets](https://arxiv.org/pdf/1211.0820.pdf) 的 Data Reuse 的部分,`avx_transpose`,不斷重複使用資料(I0 ~ I7 , T0 ~ T7),而 avx 在 Data Reuse 上的效能是勝過 sse 的
接著嘗試使用不同的 prefetch distance 來比較 `avx_prefetch_transpose` 的效能
| prefetch distance | 8 | 16 | 24 | 32 |
| ----------------- | - | -- | -- | -- |
| Average | 42850.82 | 43546.13 | 45357.07 | 47583.64 |
| Standard Deviation| 298.008 | 320.335 | 606.971 | 815.092 |
| Confidence Interval (95%) | 42254.804 - 43446.836 | 42905.46 - 44186.8 | 44143.128 - 46571.012 | 45953.456 - 49213.824 |
表格為不同 prefetch distance 個跑 100 次後其執行時間的統計結果,從資料分布來看 prefetch distance = 8 的效能最好,這裡就不考慮更大的 prefetch distance,根據論文的敘述,prefetch distance 太大會導致 cache miss 機率提升
再來重新比較 L2 cache miss/hit
| | avx | avx prefetch | sse | sse prefetch | naive |
| -- | --- | ------------ | --- | ------------ | ----- |
| DATA_RD_MISS | 307,4598 <br/> (97.822%) | 89,6375 <br/> (32.186%) | 421,5791 <br/> (99.239%) | 3,3613 <br/> (1.009%) | 1678,0959 <br/> (99.686%) |
| DATA_RD_HIT | 3,2268 <br/> (1.027%) | 185,2637 <br/> (66.522%)| 2,3144 <br/> (0.545%) | 329,1381 <br/> (98.761%) | 4,6184 <br/> (0.274%) |
| DEMAND_DATA_RD | 314,3045 | 278,4986 | 424,8124 | 333,2668 | 1683,3807 |
可以看出 prefetch 有確實提升 L2 cache hit rate,但是提升幅度沒有 sse prefetch 那麼大
最後是比較 `sse_prefetch_transpose` 與 `avx_prefetch_transpose` 執行速度,其他方法的執行速度差距明顯,所以就不特別比較
| | avx_prefetch_transpose | sse_prefetch_transpose |
| -------- | -------- | -------- |
| Average | 42750.75 | 43661.29 |
| Standard Deviation | 328.847 | 128.357 |
| Confidence Interval (95%) | 42093.056 - 43408.444 | 43404.576 - 43918.004 |
比較執行 100 次的結果,`avx_prefetch_transpose` 的執行速度是比較快的,但是卻沒有贏太多,這裡可以同樣參照 [Performance of SSE and AVX Instruction Sets](https://arxiv.org/pdf/1211.0820.pdf) 中 Asynchronous Data Transfer 的執行結果比較,avx 跟 sse 在有 prefetch 的狀況下,效能是相近的
## 調整矩陣大小做驗證
為了進一步驗證 sse 與 avx 之間的效能比較,所以針對長度從 1024 到 8192 的矩陣做不同方法的效能比較,矩陣長度以 1024 為單位做遞增,其結果如下
![](https://i.imgur.com/wTMcyAR.png)
![](https://i.imgur.com/Vv36BUM.png)
第一張圖可以很直覺看出各個方法的效能差異,sse 與 avx 的效能在不同矩陣大小下也保持著前述的結論:
- Data Reuse:avx > sse
- Asynchronous Data Transfer:avx ~= sse
而第二張圖的 y 軸使用 logscale,主要原因是第一張圖會給人成長倍率為:naive > sse > avx > sse prefetch = avx prefetch 的錯覺,但是 y 軸用成 logscale 後就可以看出其成長倍率是相近的
## 參考資料
[intel intrinsics guide](https://software.intel.com/sites/landingpage/IntrinsicsGuide/#techs=SSE,SSE2,SSE3,SSE4_1,SSE4_2&expand=3162)
[Intel® 64 and IA-32 Architectures Developer’s Manual: Vol. 3B](https://www.intel.com/content/www/us/en/architecture-and-technology/64-ia-32-architectures-software-developer-vol-3b-part-2-manual.html)
[software-pipling 共筆](https://hackmd.io/s/S1fZ1m6pe#perf-raw-counter)
[stackoverflow -- Transpose an 8x8 float using AVX/AVX2](https://stackoverflow.com/questions/25622745/transpose-an-8x8-float-using-avx-avx2)
[\_MM_SHUFFLE question](http://forums.codeguru.com/showthread.php?337156-_MM_SHUFFLE-question)
[Performance of SSE and AVX Instruction Sets](https://arxiv.org/pdf/1211.0820.pdf)