2020年5月10日 星期日

clang & gcc vector extension 探究心得

這文章換整了把一個以前納悶許久的功能做了研究與釐清後的心得 - vector selection in GCC / Clang
在 GCC / Clang 的 vector extension 中 "?:" operation 的使用, 基本上搜尋 "gcc vector extension" or "clang vector extension" 你一定會看到文件對於 ?: 這操作寫著是支援的 甚至 clang 的文件 這麼列著.

 
 
然而按照常用的 CL 使用方式去寫 code 會發現沒有辦法編譯過.
對於 OpenCL 中的使用, 請參考 Programming with OpenCL C 一書的 Vector Operators - Conditional Operator

對於 gcc/clang 支援的 ?: 是指 a ? b : c 這樣的計算中
的 a 必須是 scalar type, b, c 可以是 scalar or vector ... 因此如此是無法達到最好用的 vector element selection 功能, 也就是當我們撰寫 c = a > b ? a : b; 時於 vector 上等同於:
u32x8 c = {
    a[0] > b[0] ? a[0] : b[0],
    a[1] > b[1] ? a[1] : b[1],
    a[2] > b[2] ? a[2] : b[2],
    a[3] > b[3] ? a[3] : b[3],
    a[4] > b[4] ? a[4] : b[4],
    a[5] > b[5] ? a[5] : b[5],
    a[6] > b[6] ? a[6] : b[6],
    a[7] > b[7] ? a[7] : b[7],
};
以這個例子來說當宣告:
u32x8 a, b, c;
...
c = a > b ? a : b;
對於 x86 AVX ISA 而言你會希望 compiler 能使用下列 intrinsics / instruction:
_mm256_max_epu32 / vpmaxud
但是很不幸的是在 gcc / clang 這樣的 code 無法被編譯, 在不考慮直接使用 AVX intrincs 的情況下, 搜尋後會得到兩個可能的建議

1. 使用 loop 讓 compiler 優化, 也就是:**

for(int i = 0; i < 8; i++)
    c[i] = a[i] > b[i] ? a[i] : b[i];
如此你會得到如下圖的使用 loop 結果, 雖然有使用 AVX register, 但是計算上完全沒有好處
 

 

2. 這個結果比較需要技巧才會找到, 也就是 bitwise operation:**

u32x8 d = (u32x8)(a > b);
u32x8 c = (d & a) | (~d & b);
如此你會得到圖三的結果, 以 code generation 的結果這可能是相對較好的方式
 

然而這完全比不上直接使用 avx intrinsics 的下圖的結果:
u32x8 c = _mm256_max_epu32(a, b);

 

g++/clang++

然而還是有最後希望 - GCC/g++, 事實上 gcc 文件中如下圖有提到 C++ 模式支援 element-wise


由於個人印象中並沒有這樣的方式, 查了一下 是 GCC 5 / Clang 10 開始支援這樣的模式 (只能說在最需要使用的時候 android gcc, Google 只用到了 4.9 ... 之後的版本 vector extension 沒特別去用, 加上逐漸重心轉到使用 clang), 如此可以支援一開始期望的語法, 而且 code generation是很漂亮的(g++:下圖1/clang++:下圖2, g++ 的比較好), 非 clang 不可時透過 g++/clang++ 編譯產生 .s 再整入 clang project 是唯一可以考慮的作法 (否則就是 code 可讀性與移植性變差 + 努力查 intrinsics table 了)
1. 
 

2.

 
除此之後後續做了些實驗額外得知了一些限制/資訊

gcc & clang compatibility

在先前 gcc / clang 各自有著自己的 vector extension 與宣告方式, 個人也針對兩者做了不同宣告, 然而有趣的地方實驗過程中, 曾經忘了切換, 然而可以順利編譯過, 這也表示 gcc / clang 互相吃了對方的 vector extension 的宣告方式, 因此個人一開始以為在各自 compiler 上兩個 vector extension 應該是互通的. 這點在昨日得知 clang 在實作時會考量儘量與 gcc 一致, 猜想 gcc 在一些特性上也對 clang 做了類似的考量.

OpenCL-like vector swizzle

這是被詢問的問題, 因此注意到了差異, 基本上只有 clang 提供了接近 OpenCL 的 vector swizzle 方式:
u32x4 a, b, c;
...
c.s02 = a.s02; // or c.xz = a.xz;
c.s13 = b.s02; // or c.yw = b.xz;
當然還可以針對特定 vector lane 作 value assign.
比較麻煩的是 initial value, clang 並沒有提供如同 OpenCL 的彈性, 而且必須是以 { ... } 的方式一一擺放, 而不如 OpenCL 以 ( ... ) 且能夾雜大小不同的 vector 作為數個數值. 在 clang 上要使用這個特性, 必須使用 clang 專屬的 vector extension 宣告方式.

vector as array

最早是 clang 開始提供的, 而且不只是 vector extension, 對於 SIMD intrinsics 的 data type clang 都支援. 這個功能相當實用, 在 android 7 時期導入 clang 可以說幫了大忙, 這點對於 C Model 與 SIMD 版本實作的轉換與驗證簡化不少.
使用上類似:
u32x4 a, b, c, vout;
...
vout = fa*a + fb*b + c;
for(int i = 0; i < 4; i++){
    assert(out[ofs+i] == vout[i]);
}
後續 gcc 也在 vector 支援了這個功能, 然而必須使用 gcc 自己的 vector extension 宣告方式

沒有留言:

在 ARM 平台上使用 Function Multi-Versioning (FMV) - 以使用 Android NDK 為例

Function Multi-Versioning (FMV) 過往的 CPU 發展歷程中, x86 平台由於因應各種應用需求的提出, 而陸陸續續加入了不同的指令集, 此外也可能因為針對市場做等級區隔, 支援的數量與種類也不等. 在 Linux 平台上這些 CPU 資訊可以透過...