在套用 gcc / clang 的 -ftree-vectorize or compiler vector extension 時來產生適當的 SIMD optimization 時, 同時會產生一個問題 - 當編譯時套用的 architecture 參數(e.g.: -mavx2) 所使用的指令集與運作時的 CPU 不一致時可能會出現 SIGILL (illegal instruction) 的問題. 過往可能會採用 CPU feature detection + dispatching code 的撰寫, 大致上邏輯如下:
// AVX-512 versionvoid __foo_avx512(void){ ... }// AVX versionvoid __foo_avx2(void){ ... }// SSE4 versionvoid __foo_sse4(void){ ... }// C versionvoid __foo(void){ ... }void foo(void){if(_avx512_available())__foo_avx512();else if(_avx2_available())__foo_avx2();else if(_sse4_available())__foo_sse4();else__foo();}
這是過去長久以來很平常的作法, 而最近研究在接觸 Clear Linux, 研究了一下 Intel 採用的方式, 發現了有趣的事(這些 Intel 都有在 2016 年的簡報 - Improving Linux Performance with GCC latest technologies 中說明), 主要是 gcc 4.8 中已新增了 Function Multi-Versioning (FMV)的功能, 能透過函數屬性更方便地做到原本的目的, 以上述的例子來說就會變成:
// AVX-512 version__attribute__ ((target ("avx512f")))void foo(void){ ... }// AVX2 version__attribute__ ((target ("avx2")))void foo(void){ ... }// SSE4 version__attribute__ ((target ("sse4.2")))void foo(void){ ... }// C version__attribute__ ((target ("default")))void foo(void){ ... }
使用 FMV 功能的好處首先所有的 function 都是相同的名稱, 僅是屬性中的 target 不同, 而GCC 會自動產生偵測 CPU 功能並從中選擇對應 foo 的 code, 這點可以省去維護上面 dispatching code 的問題(若有多個函數, 增刪都會是很冗長的工作). 使用 __attribute__ ((target ("TARGET_NAME"))) 的語法方式目前已被 gcc / icc / clang 所採用.
然而並不是所有人都有心力以 SIMD ISA 去實作版本, 這時複製多份相同的 C code 來做不同 SIMD ISA 的 auto-vectorization 似乎不是很實際的方式, 因此 gcc 進一步提供了有趣的屬性功能 - target_clones:
// C version__attribute__ ((target_clones ("avx512f", "avx2", "sse4.2", "default")))void foo(void){ ... }
而 GCC FMV 功能亦能夠與 compiler vector extension 合併使用, 因此以 compiler vector 實作後函數可以使用上述 target_clones 的方式, 如此 compiler 會自動針對不同的 SIMD ISA 生出多版本的, 以個人常用來教學的 tiled 8x8 gemm 來說就會像是:
#define TSIZE 8接著以 gcc 編譯:
#if defined (__clang__)
typedef float vfloat __attribute__((ext_vector_type(TSIZE)));
#else
typedef float vfloat __attribute__ ((vector_size (TSIZE*4)));
#endif
__attribute__ ((target_clones ("avx512f", "avx2", "avx", "sse4.2", "default")))
void gemm_vec(float *a, int sa, float *b, int sb, float *c, int sc)
{
vfloat vb[TSIZE];
for(int y = 0; y < TSIZE; y++){
vb[y] = *((vfloat*)(b + sb*y));
}
for(int y = 0; y < TSIZE; y++){
vfloat vc = *((vfloat*)(c + sc*y));
vfloat va = *((vfloat*)(a + sa*y));
for(int x = 0; x < TSIZE; x++){
vc += va[x] * vb[x];
}
*((vfloat*)(c + sc*y)) = vc;
}
}
$ gcc -O3 -shared mm.c -o libgemm.so
透過 objdump 觀察
$ objdump -t libgemm.so
可以看到中間有如下的輸出:
0000000000001720 l F .text 0000000000000c96 gemm_vec.default.4
00000000000023c0 l F .text 00000000000005df gemm_vec.avx512f.0
00000000000029a0 l F .text 00000000000005df gemm_vec.avx2.1
0000000000002f80 l F .text 000000000000060c gemm_vec.avx.2
0000000000003590 l F .text 0000000000000c95 gemm_vec.sse4_2.3
此外值得一提的是在 2019 年初 Clear Linux 計劃中 Intel 釋出了一個 FMV patch generator, 透過 make-fmv-patch 這個工具能針對 C/C++ code 自動產生對應的 FMV 屬性 patch, 套用後重新編譯即可產生一體適用的 SIMD optimized libraries / executables
沒有留言:
張貼留言