顯示具有 clang 標籤的文章。 顯示所有文章
顯示具有 clang 標籤的文章。 顯示所有文章

2022年3月17日 星期四

Clang 14.0 將增加 vector extension built-in functions

由於 LLVM / Clang 14.0 發佈的接近, 近日就花了點時間看一下對於 vector extension 是否有相關的更新. 目前在 Clang 官方網站文件上已經可以看到.

Clang 14.0 中最令人振奮的莫過於新增的兩個部份

  • Vector Builtins
  • Matrix Types

Vector Builtins

vector builtins 主要能提供 compiler 明確使用對應指令來加速, 而這些不容易透過 C operator 來表示, builtins 分為兩類 elementwise builtins 與 reduction builtins 

Fig. 1 - Elementwise Builtins 列表

Elementwise Builtins 本質上就是 Per-lane operation, 像是 __builtin_elementwise_max, 事實上透過先前分享過的 Clang ternary operator 就可以實作, 也就是說下列的 ternary operation:

vc = va > vb ? va : vb;

基本上等同於:

vc = __builtin_elementwise_max(va, vb);

事實上 elementwise builtins 並不限定於套用在 vector type, 基本的 scalar types 也可以.

Fig. 2 - Reduction Builtins 列表

Reduction Builtins 主要用在可以快速從 vector 中算出單一數值的結果, 舉凡像是總和(sum), 最大值(max)與最小值(min) 等等. 在 Clang 中還有特別的 - 所謂水平方向 AND / OR / XOR. 這對一些特別的應用很有幫助.

Matrix Types

由於現今新一代的 CPU 都加入了對於 matrix 操作加速的指令(Intel 的 AMX, ARM 的 SVE Matrix Extension 與 SME), 因此 Clang 加入 matrix type 也有助於更有效去使用這些.

在 Clang 中可以透過下列方式定義與使用 Matrix:

Fig. 3 - Clang Matrix Type 定義與使用範例

透過定義 Matrix Type, 能夠直接支援基本運算操作. 目前官方說明與範例中並沒有顯示與 vector 的互動. 但以目前的型態撰寫相關應用已便利不少.



2017年1月6日 星期五

你也可以寫 SIMD 比寫網頁還快 - III

如果沒有特別的後續想法
這篇就是系列文的最後一篇
"你也可以寫 SIMD 比寫網頁快" 的精神
不外乎就是 簡單 快速 有效

延續上兩篇在最後想要談的是搭配 clang + OpenCL vector 的進階使用方式
這裡只探討一個問題 : 使用那些基本的運算符號是否就能用上所有的 SIMD 指令?
很明顯的, 答案是不行(否則就不會有這篇啦 XDDDD)

但是如果你知道特定好用的 SIMD 指令該如何套用呢?
舉例來說 ARM NEON 有個特別的指令 VQDMULH
它的用途是將兩個 16/32bit 數值相乘後所產生的 32/64bit 數值, 取其高位的 16/32bit
以 code 表示的話:
int a, b, c;
...
c = sqdmulh(a, b);
// equal-to
int64 tmp;
tmp = a*b;
c = (int)(tmp>>32) ;
這指令對於 NEON 上實作常數的除法, 或是 fixed-point 的實作上很有幫助
這時我們可以這麼做:
int4 va = {0, 1, 2, 3};
int4 vb = {1, 2, 3, 4};
int4 vrab = vqdmulhq_s32(va, vb);
上面的範例中 int32x4_t 為 NEON intrinsics 的 data type
而基本的精神並不是什麼難以理解的事
僅是透過 pointer 作 NEON vector 與 OpenCL vector 的型別替換使用
而在之後要換回使用 OpenCL vector 可以透過一樣的方式轉回
接著我們可以透過 vec.s 觀察 clang code generation 的結果:
        adr     r0, .LCPI2_0
...
        vld1.64 {d16, d17}, [r0:128]

...
        adr     r0, .LCPI2_1
        vld1.64 {d18, d19}, [r0:128]
        vqdmulh.s32     q8, q9, q8
在此為了確認這些暫存器間的關係, 可以參考 (ARMv7) NEON registers
64b 的 (d16,d17) 即為 128b 的 q8
64b 的 (d18,d19) 即為 128b 的 q9
所以可以看到這樣的作法並沒有因此產生多餘的指令
是不是很簡單呢?

在此 "你也可以寫 SIMD 比寫網頁還快" 系列文告一段落

你也可以寫 SIMD 比寫網頁還快 - II

第一篇並沒有介紹 clang 中的 OpenCL kernel 一些基礎 型別宣告部分, 已經在上一篇出現過了, 所以這裡就從使用開始. 首先是 vector 的初始化

typedef int int4 __attribute__((ext_vector_type(4)));
typedef int int8 __attribute__((ext_vector_type(8)));
int a = 3;
int4 va = {1, 2, 3, 4}; 
int4 vb = 3;

接著是個別的 access, 下列的方式很適合做資料格式上的操作 在等號的兩邊可以用不同的表示法, 沒有需要一致:

va.x = a; 
// the below two lines are equivalent. 
vb.xyw = va.xzw; 
vb.s013 = va.s023; 
// repeat is ok 
va = vb.xyyz; 
// high and low part 
va.hi = va.lo;
va.lo = vb.hi;
// even and odd 
va.odd = vb.even;

Vector 簡單的計算如下, 可以參照 clang - vectors and extended vectors 的列表:

// scalar multiply 
va = a * vb;
// element-wise multiply
va = va * vb;
va = va | vb;
va = -va;

不同型別的 vector 轉換:

typedef float float4 __attribute__((ext_vector_type(4))); 
// format convertion 
float4 vfa = __builtin_convertvector(va, float4); 
// data reinterpret
float4 vfa = (float4)va;

接著來實戰一番吧, 下列為以 bayer format 平均 GRBG 4個像素, 轉為1/4大小灰階圖的範例

void bayer_convert(char *src, char *gray, int w, int h) 
{
    for(int y = 0; y < h; y+=2){ 
        for(int x = 0; x < w; x+=16){
            //bayer raw data fetch
            ushort8 pix00 = __builtin_convertvector(*((uchar8*)(src+x)), ushort8);
            ushort8 pix01 = __builtin_convertvector(*((uchar8*)(src+x+8)), ushort8); 
            ushort8 pix10 = __builtin_convertvector(*((uchar8*)(src+x+w)), ushort8);
            ushort8 pix11 = __builtin_convertvector(*((uchar8*)(src+x+w+8)), ushort8);

            ushort8 pix_g0;
            pix_g0.lo = pix00.even;
            pix_g0.hi = pix01.even; 
            ushort8 pix_r;
            pix_r.lo = pix00.odd;
            pix_r.hi = pix01.odd;
            ushort8 pix_b;
            pix_b.lo = pix10.even;
            pix_b.hi = pix11.even;
            ushort8 pix_g1;
            pix_g1.lo = pix10.odd;
            pix_g1.hi = pix11.odd;

            // average! so simple
            ushort8 pix_out = (pix_g0 + pix_r + pix_b + pix_g1) ;
            // write out 
            *((uchar8*)(gray+(x/2))) = __builtin_convertvector(pix_out, uchar8); 
        }
        src += w*2; 
        gray += w;
    }
}
如何? 比起看 intrinsics function 來得清楚與簡單吧
如果還想了解 OpenCL vector 的操作可以參考這篇文章
有興趣記得編譯時加入 --save-temps 來看使用的指令!!! 下一篇會介紹進階的使用方式


2017年1月5日 星期四

你也可以寫 SIMD 比寫網頁還快 - I

看到 Jim Huang 要開”從無到有自幹 boot loader 比寫網頁還快” 除了幫他打廣告外, 也趕快搭這個熱潮, 證明人人可以寫 SIMD 比網頁還快 去年寫了系列 MMX/SSE 去處理 Matrix Transpose 但是看到 SSE/AVX intrinsics 很多人大概就眼睛花了 若加上實際應用還要從 Intel 網頁查找適當的 intrinsics 會覺得難以下手 搭配適當的工具, 你也可以是 SIMD 高手! 這裡最重要的工具是 - clang clang 如同 gcc 一般提供了許多 extension 其中很重要的一點是 Vectors and Extended Vectors 這個 extension 請特別注意 link 中的 OpenCL vector, 底下的表格表示此方式提供相當全面的 operator 而 clang 中使用 OpenCL vector 這即是這裡所推荐的方式 (還可以學會 OpenCL kernel 的部份撰寫!) 首先來以 clang 方式定義 OpenCL vector 的型別
typedef int int8 __attribute__((ext_vector_type(8)));
UPDATED: 對於 GCC 支援相近語法, 其宣告為 (其中 vector_size 以 byte 計)
typedef int int8 __attribute__ ((vector_size (32)));

這裡定義了長度為 8 的 int vector type 接著我們來實作先前時做過的 matrix transpose 功能
void transpose8x8(int *src, int *dst, int w, int h){
    for(int x = 0; x < w; x+=8){
        for(int y = 0; y < h; y+=8){
            int8 row0 = *((int8*)(src+y*w+x));
            int8 row1 = *((int8*)(src+(y+1)*w+x));
            int8 row2 = *((int8*)(src+(y+2)*w+x));
            int8 row3 = *((int8*)(src+(y+3)*w+x));
            int8 row4 = *((int8*)(src+(y+4)*w+x));
            int8 row5 = *((int8*)(src+(y+5)*w+x));
            int8 row6 = *((int8*)(src+(y+6)*w+x));
            int8 row7 = *((int8*)(src+(y+7)*w+x));
            int8 tmp0, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7;
            tmp0 = (int8){row0.s0, row1.s0, row2.s0, row3.s0, row4.s0, row5.s0, row6.s0, row7.s0};
            tmp1 = (int8){row0.s1, row1.s1, row2.s1, row3.s1, row4.s1, row5.s1, row6.s1, row7.s1};
            tmp2 = (int8){row0.s2, row1.s2, row2.s2, row3.s2, row4.s2, row5.s2, row6.s2, row7.s2};
            tmp3 = (int8){row0.s3, row1.s3, row2.s3, row3.s3, row4.s3, row5.s3, row6.s3, row7.s3};
            tmp4 = (int8){row0.s4, row1.s4, row2.s4, row3.s4, row4.s4, row5.s4, row6.s4, row7.s4};
            tmp5 = (int8){row0.s5, row1.s5, row2.s5, row3.s5, row4.s5, row5.s5, row6.s5, row7.s5};
            tmp6 = (int8){row0.s6, row1.s6, row2.s6, row3.s6, row4.s6, row5.s6, row6.s6, row7.s6};
            tmp7 = (int8){row0.s7, row1.s7, row2.s7, row3.s7, row4.s7, row5.s7, row6.s7, row7.s7};
            *((int8*)(dst+(x*h)+y)) = tmp0;
            *((int8*)(dst+((x+1)*h)+y)) = tmp1;

            *((int8*)(dst+((x+2)*h)+y)) = tmp2;
            *((int8*)(dst+((x+3)*h)+y)) = tmp3;
            *((int8*)(dst+((x+4)*h)+y)) = tmp4;
            *((int8*)(dst+((x+5)*h)+y)) = tmp5;
            *((int8*)(dst+((x+6)*h)+y)) = tmp6;
            *((int8*)(dst+((x+7)*h)+y)) = tmp7;
        }
    }
}
將上列內容與先前的範例結合, 最後就是編譯了
clang -O3 -mavx -mfma -o test test.c
 以下為在 Intel(R) Core i7-3612QM 的執行結果
clang vector: 35759 us
sse: 41246 us
naive: 146310 us
這樣是不是很快呢? 甚至比之前直接用 SSE intrinsics 寫的還快!!!(灑花) 更棒的是這樣的 SIMD 撰寫, 透過 clang 是可以在各種硬體平台的 可以自己試試看用 NDK 然後跑在自己的手機看看喔!!

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

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