contributed by <tina0405
>
中英文字間請以空白隔開
課程助教
好,已改。謝謝助教 tina0405
(TABLE V)只有軟體預取沒有硬體的情形下,各個 baseline 上的執行時間。
此篇論文也丟出3個問題:
在 steaming prefetch 和 GHB prefetch,intrinsics 上目前還沒看到明確的解釋
TABLE.I說不只針對普通的資料結構還有特別的,例如:RDS(Recursive Data Structure)insert prefetch intrinsics。
註:Temporal — Data will be used again soon
參考論文 When Prefetching Works, When It Doesn’t, and Why 重點提示和解說
Software Prefetch Intrinsics:
關於這裡的直接和間接:
我的理解是
直接:直接得到(eg.A[i])的東西。
間接:透兩層以上值的傳遞(eg.B[A[i]])才得到。
預取分為六類:
Software Prefetch Distance:
Direct and Indirect Memory Indexing
想法:
預取就像間接的索引,會有更多的 overhead,但我之前一直覺得有很多的 overheads 是應該會更慢嗎?可是忽略了其實預取解決了 latency 的問題,在L1->L2->L3->中找不到所發生的cache miss,再回到主記憶體去找的 latency 是比 overhead更久的。
軟體預取的好處(比起硬體):
軟體預取的負面衝擊
增加指令數:
關於靜態的插入
就是因為再軟體內沒有一套固定的機制(不像硬體),所以也沒有一種可以適應的改變,那些變化的 memory latency 或 快取的大小也都無從掌握,導致插入靜態參數的困難。
程式結構的改變
本來在 indirection 的時,軟體的不規則是優點,但在insert 這裡,反而變成了缺點。
在這邊我對 training 一詞有不太理解的地方,如果以之前影像處理所說的 training 就是跑過一次後記住那些特徵,但這邊難道也是硬體跑過後要去紀錄哪個指令特別快或慢,再去補足嗎?
軟體和硬體合用卻反效果
實驗方法
因為還是不是很理解,想拿實際例子看看:
自己的理解p refetch:
在 I/O 發出時脈前,就先將他就先將所需的資料存入快取,當資料真正被需要時,
就可以很快地拿出來比起去 request from memory 快很多。
Software Prefetch Intrinsics:
文中解釋,是 X86 SSE SIMD延伸的一部份,後面的例子有舉出軟體預取的函式說明。(會在論文筆記中討論)
參考 迴圈展開:
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:
1.讀取指令
2.指令解碼與讀取暫存器
3.執行
4.記憶體存取
5.寫回暫存器
頻寬(Bandwidth):傳輸媒介的最大吞吐量(throughput)。
如同上面所說的如果當 Bandwidth 就只有這麼大,那我軟體增加的指令數就會佔用有限的頻寬。
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
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
naive_transpose
, sse_transpose
, sse_prefetch_transpose
之間的效能差異,以及 prefetcher 對 cache 的影響先看一下定義:參考Microsoft
Loads 128-bit value.
__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.
__m128i _mm_unpacklo_epi32 (__m128i a, __m128i b);
/*Return Value
r0 := a0 ; r1 := b0
r2 := a1 ; r3 := b1*/
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);
}
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);
}
}
}
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_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。
Makefile
,產生新的執行檔,分別對應於 naive_transpose
, sse_transpose
, sse_prefetch_transpose
(學習 phonebook 的做法),學習 你所不知道的 C 語言:物件導向程式設計篇 提到的封裝技巧,以物件導向的方式封裝轉置矩陣的不同實作,得以透過一致的介面 (interface) 存取個別方法並且評估效能naive
, sse_transpose
, sse_prefetch_transpose
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
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);
target:dependency
command
target(目標):一個目標檔,可以是Object檔,也可以是執行檔。還可以是一個標籤(Label)。
dependency
(依賴):要產生目標檔(target)所依賴哪些檔。
command(命令):
建立專案時需要執行的shell命令
注意:
command部分的每行的縮進必須要使用Tab鍵,就算用空白推到相應位置也不行,會出現紅色警示。
最後要記得執行檔也要clean:
Clean:
$(RM) naive sse ssep
想說改成下面:(直接跑結果)
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
perf stat
的 raw counter 命令