快快樂樂SIMD
Weita, Wang
什麼是SIMD
SIMD???
cv::Mat a(4,4,CV_32FC1);
cv::Mat b(4,4,CV_32FC1);
cv::Mat c(4,4,CV_32FC1),d;
d= a*b+c;
Eigen::MatrixXf a(3,3);
Eigen::MatrixXf b(3,3);
Eigen::MatrixXf c(3,3),d;
d=a*b+c;
記憶體 暫存器
編譯器
什麼是SIMD
• C/C++專用極限優化
• 指標only
• 必須明確定義暫存器、記憶體、編譯器行
為才可以挑戰極限
C/C++層
組合語言層
SIMD
C=A+B ?
float arr0[4] = { 1,2,3,4 };
float arr1[4] = { 5,6,7,8 };
float arr2[4] = { 0 };
A
B
C
A BC +=
Result: arr2[4] => { 6,8,10,12 };
SIMD為什麼快
for(int i=0;i<4;i++)
arr2[i]=arr0[i]+arr1[i];
for(int i=0;i<4;i++)
*(arr2 + i) = *(arr0 + i)+*(arr1 + i);
1 1*4
(1+1)*4 (1+1)*4 (1+1)*4
37 cycles
1*4
1*4
1. 假定一個指令一個cycle
2. address shift列入考量
SIMD為什麼快
float32x4_t a,b,c;
a=*(float32x4_t *)arr0;
b=*(float32x4_t *)arr1;
c=a+b;
*(float32x4_t *)arr2=c;
4 cycles9倍
SIMD的第一步
你必須要會算cycle數
變數架構
dobule a; //64bits
float b; //32bits
int c; //32bits
short d; //16bits
char e; //8bits
unsigned int f; //32bits
unsigned short g; //16bits
unsigned char f; //8bits
….
• SSE
__m128d aa;
__m128 bb;
__m64d cc;
__m64d dd;
• NEON
float64x2_t aa;
float32x4_t bb;
int32x4_t cc;
int16x8_t dd;
int8x16_t ee;
…
暫存器架構
64
32 32 32 3232 32
64 64
16 16 16 16 16 16 16 16
8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8
0 64bits 0 128bits
一般暫存器
General Purpose Register
Scale Register
向量暫存器
Vector Register
1x
1x
2x
4x
16x
8x
重新定義
重新定義變數
• SSE
typedef __m128d double2;
typedef __m128d float4;
typedef __m64d float2;
typedef __m128 int4;
typedef __m64 int2;
typedef __m128 uint4;
typedef __m64 uint2;
….
• NEON
typedef float64x2_t double2;
typedef float32x4_t float4;
typedef float32x2_t float2;
typedef int32x4_t int4;
typedef int32x2_t int2;
typedef uint32x4_t uint4;
typedef uint32x2_t uint2;
….
重新定義暫存器
union reg128 {
uchar16 _uchar16;
short8 _short8;
int4 _int4;
float4 _float4;
double2 _double2;
uchar _uchar[16];
short _short[8];
int _int4;
float _float[4];
double _double[2];
…
void print_uchar()
{
printf(“%d %d..n”,
_uchar[0],_uchar[1],_uchar[2]….);
}
void print_float()
{
printf(“%f %f %f %fn”,
_float[0],_float[1]...);
}
…
};
查找指令
SIMD的第二步
記憶體行為
記憶體
• 在接近奈米極限(10nm)的時代,CPU運算已經
不是瓶頸,每年CPU運算都在變,唯一不變的
是記憶體傳輸速度
• 多資料的並行,SIMD成功的讓cpu cycle數再
少4倍
• 資料由記憶體L2L1register Load/Store
產生的延遲
記憶體階層延遲
Reference: https://tinyurl.com/gsnfzoy
L1 cache
• 在function內開的array
• 暫存器使用超過既定數量多的資料會藉由stack
pointer寫回L1 cache
• Function引數資料傳遞(部分-O3優化會穿透,不寫回)
• Function call儲存目前暫存器資料,結束後讀出
• interrupt, 換thread,寫回目前暫存器資料,視作業系
統能力而定
I-Cache/D-Cache
• Instruction Cache:
– CPU指令集的總大小(function symbol
size),function第一次執行不會預載,第二次執行
的時候會in cache,如做computer vision應用,第一
次執行通常都忽略不計
• Data Cache:
– 即L1 cache,通常用既定的方法論將資料預先放
入L1 cache
Page Table
• 一個page  4096 bytes
• 一個cache line64 bytes
• 一個page包含64個cache line
• L2 cache  5~10Mb
• L1 cache  512k~1Mb
• L1 entry way 2 way or 4 way
• 一張影像320*240 or 640*480 bytes
• 是否爆掉?????
Cache line
• 64byte= 16 個float
• 128bits=4個float
64位定址
64位定址
世界觀
• SIMD的世界裡,若達到極限,查表法不會再是
優化手段,使用vector register直接算起碼可
少4倍cycle數,若表很大,速度反而變快
• 在極限優化的前提,只要有微小的Load/Store
行為,都影響很大!
已知方法論
int arr0[100] = {1,2,3…};
void test1 (float *src,float *dst,int len)
{
int arr1[100] = {1,2,3…};
int b =4;
int *arr2 = (int *)malloc(100*sizeof(int));
int c = len + b;
…
}
Memory
L1 cache
Instruction set
Const
Memory
已知方法論
class a
{
int val = 3;
int map[100] = {1,2,3,4,5};
a();
…
};
Memory
同等於struct
編譯器沒你想的聰明
void test0(float *src_dst,int len)
{
float4 *src_dst_ptr = (float4 *)src_dst;
float4 cc=*src_dst_ptr + *src_dst_ptr;
*src_dst_ptr +=cc;
…
}
三次Load行為
一次Store行為
正確寫法
void test0(float *src_dst,int len)
{
float4 *src_dst_ptr = (float4 *)src_dst;
float4 val = *src_dst_ptr;
float4 cc= val + val;
*src_dst_ptr =cc + val;
…
} 一次Load行為
一次Store行為
少用陣列,多用pointer++
void test1(float *src,float *dst,int len)
{
float4 *src_ptr =(float4 *)src;
float4 *dst_ptr=(float4 *)dst;
float4 reg0,reg1…;
for(int i=0;i<len;i+=4)
{
reg0=*src_ptr++;
reg1=*src_ptr++;
reg0 = reg0+reg1;
…..
*dst_ptr++=reg0;
*dst_ptr++=reg1;
}
}
• 不建議使用
void test2(float4 *src,float4 *dst,int len)
{
int len_4 = len/4;
float4 reg0,reg1…;
for(int i=0;i<len_4;i+=2)
{
reg0=src[i]+src[i+1];
…
dst[i]=reg0;
dst[i+1]=src[i+1];
}
}
自我運算,
避掉cache miss/Page fault
void test1(float *src_dst, int len)
{
float4 *src_dst_ptr =(float4 *)src_dst;
float4 reg0,reg1…;
for(int i=0;i<len;i+=4)
{
reg0=*src_dst_ptr++;
reg1=*src_dst_ptr++;
reg0 = reg0+reg1;
…..
*src_dst_ptr++=reg0;
*src_dst_ptr++=reg1;
}
}
Align/Unalign
• cache line 64 bytes,且16位定址對齊
• 向量暫存器load/store 沒在address 16倍數上
– 延遲懲罰
– 視cpu架構而定,大部分都會
0x0000 0x0010
8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8
0x0020
由這邊開始Load/Store128bits
解決方法
• 選擇內建指令(unalign)
• 對齊load/store,利用alignr or vext組裝
• malloc or 宣告陣列用16位對齊
32 32 32 32 32 32 32 32
reg0
reg3=vext(reg0,reg1,1)
reg1
float __attribute__ ((aligned (16))) a[40];
float *b=(float *)malloc(sizeof(float)*40);
b= (float*)(((unsigned long)b + 15) & (~0x0F))
SIMD的第三步
暫存器行為
64
32 32
0 64bits
32 32 32 32
64 64
16 16 16 16 16 16 16 16
8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8
0 128bits
暫存器
• Arm64
– Vector Register: 32個
– Scale Register: 32個
• Arm32
– Vector Register: 16個
– Scale Register: 32個
• Intel SSE
– Vector Register: 16個
– Scale Register: 32個
• DSP
– Vector Register: ?個
– Scale Register: ?個
必須記住暫存器數目 (很重要!!)
Why
• 假設全float的前提下arm64 vector可提供
– 128個空間大小模擬陣列(4*32)
– 完全不用寫回記憶體的極限操作
• 配合shuffle使用
– 若同時使用超過32個變數,多的資料會寫回L1
cache,造成延遲
float arr[4*32+4] = {…};
float4 *arr_ptr = (float4 *)arr;
float4 a0,a1,a2,a3,a4,a5,a6 … a32;
A0 = *arr_ptr++;
A1= *arr_ptr++;
…
A32 = *arr_ptr++;
超過32個暫存器變數同時使用,
運算過程中會產生額外的
Load/Store 行為,無法被優化
關於暫存器
• 暫存器本身沒有型別,只有在組合語言指令
階段才定義
• 使用變數保留資料的時候,不得超過CPU提供
的最大暫存器數量,但要充分使用
• Vector Register
– 善用shuffle
– Input/Output 資料重排
Load/Store 行為
float4 *src_ptr = (float4 *)src;
float4 *dst_ptr=(float4 *)dst;
reg128 reg0,reg1,reg2,reg3… reg31;
for(int i=0;i<640*480;i+=4) {
reg0._float4 = *src_ptr++;
reg1._float4 = *src_ptr++;
reg2._float4 = *src_ptr++;
….
..
*dst_ptr++=reg0._float4;
*dst_ptr++=reg1._float4;
*dst_ptr++=reg2._float4;
….
}
一般暫存器
定址(2個)
向量暫存器32個
(假設全用)
一般暫存器
累加(1個)
一次全讀取
一次全寫回
主演算法
Function call行為
void test1(float *src,float *dst,int len) {
int a= len/4;
int b= len%4;
float4 aa = *(float4 *)src;
float4 bb = *(float4 *)dst;
float4 cc = aa + bb;
int val=test2(src,dst,len);
cc = aa + bb + cc;
int c =(a+b+len)*val;
…
}
一般暫存器
寫回L1 cache
產生Load/Sotre行為
向量暫存器
寫回L1 cache
產生Load/Sotre行為
從L1 cache 讀取
src,dst address
給一般暫存器
清空目前暫存器資料
進入test2()內產生
Load/Store 行為
從L1 cache內讀取引
數資料到暫存器 從L1 cache讀取資料
返回原始向量/一般
暫存器狀態
Stack Pointer
堆疊暫存器管理
Function Argument行為
void test3(float4 aa,float4 *bb,float4 &cc) {
…
}
void test4(float a,float *b,float &c) {
…
}
int main() {
float4 aa = { 0,0,0,0 },bb={1,1,1,1},cc = {2,2,2,2};
float a = 0,b=1,c=2;
test3(aa,&bb,&cc);
test4(a,&b,&c);
}
O3前提
產生Load/Store行為
產生Load/Store行為
產生Load/Store行為
產生Load/Store行為
直接穿過!
直接穿過!
重點
• Call by address, call by reference必經過L1
除非inline成功,一定慢
• 減少function使用,一路到底
分支指令行為
float a[100],b[100];
for(int i=0;i<100;i++)
{
if(a[i]<50)
b[i]=a[i];
else
b[i] = 30;
}
1 100 100
(1+1+1)*100
(1+1)*100*2
(1+1)*100
1101 cycles
分支指令行為
float4 *a_ptr = (float4 *)a,*b_ptr=(float4 *)b;
float4 cmp = {50,50,50,50};
reg128 val0;
reg128 reg0,mask,tmp0,tmp1;
val0._float4 = { 30,30,30,30 };
for(int i=0;i<100;i+=4)
{
reg0._float4 = *a_ptr++;
mask._uint4=vcltq_f32(reg0._float4,cmp);
tmp0._uint4=vandq_u32(reg0._uint4,mask._uint4);
mask.uint4 = vnotq_u32(mask);
tmp1._uint4=vandq_u32(val0._uint4,mask._uint4);
reg0._uint4=vxor_u32(temp0._uint4,temp1._uint4);
*b_ptr++ = reg0._uint4;
}
1+2*25
9*25
1
1
278cycles3.96倍
解析SIMD分支
float4 cmp = {50,50,50,50};
reg128 val0;
reg128 reg0,mask,tmp0,tmp1;
val0._float4 = { 30,30,30,30 };
for(int i=0;i<100;i+=4)
{
reg0._float4 = *a_ptr++;
mask._uint4=vcltq_f32(reg0._float4,cmp);
tmp0._uint4=vandq_u32(reg0._uint4,mask._uint4);
mask.uint4 = vnotq_u32(mask);
tmp1._uint4=vandq_u32(val0._uint4,mask._uint4);
reg0._uint4=vxor_u32(temp0._uint4,temp1._uint4);
*b_ptr++ = reg0._uint4;
}
if(a[i]<50) b[i]=a[i];
else b[i] =30;
11..1 00..0 11..1 00..0
If true
32個1
If false
32個0
0 128
0000 1111
0011 0100
0000 0100
AND
1111 0000
1011 0001
101 1 0000
NOT
AND
1011 0000
0000 0100
101 1 0100
XOR
分支指令行為
• 比一般比較運算快四倍
• 不存在分支預測,CPU pipeline不再因為預測
失誤而被清空,流水線一路到底(爆炸快)
Shuffle行為
• 茫茫指令海,找最適合的shuffle
– 數學模型極限優化的關鍵
– 不會寫shuffle,別跟我說你會寫SIMD
Shuffle行為
• Ex:矩陣轉置
Shuffle行為
4 cycles
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
1 5 3 7
2 6 4 8
reg0
reg1
reg2
reg3
reg0
reg1
vtrnq
9 13 11 15
10 14 12 16
reg2
reg3
1 5 9 13
2 6 10 14
reg0
reg1
3 7 11 15
4 8 12 16
reg2
reg3
vtrn
Shuffle行為
for(int i=0;i<4;i++)
for(int j=i;j<4;j++)
{
int index0=i*4+j,index1=j*4+i;
float temp=a[index0];
a[index0]=a[index1];
a[index1]=temp;
}
(4+3+2+1)*3
4 41
(1+1)*10
(1+1)*10
(1+1)*10*2
159 cycles
(1+1+1+1)*10
Shuffle行為
reg256 temp0,temp1;
reg128 reg0,reg1,reg2,reg3;
temp0._float4x2=vtrnq_f32(reg0._float4,reg1._float4);
temp1._float4x2=vtrnq_f32(reg2._float4,reg3._float4);
float2 temp =temp0._float2[1];
temp0._float2[1]=temp1._float2[0];
temp1._float2[0]=temp;
temp=temp0._float2[3];
temp0._float2[3]=temp1._float[2];
temp1._float[2]=temp;
4 cycles
vtrn
vtrn
39.75倍
Shuffle行為
• Ex:矩陣乘法
transpose 4 cycles
mul 16 cycles
vpadd 12 cycles
型別轉換
• uchar16short8int4float4
• float4int4uchar16
Image
32 32 32 32
16 16 16 16 16 16 16 16
8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8
16 16 16 16 16 16 16 16
32 32 32 32
SIMD的第四步
編譯器行為
O3優化方法論
• Clang gcc ???
• 新版本 >>>>> 舊版本
Lantency && Throughtput
• 連續Load or 連續Store降低延遲
• 特定pipeline,排列能得到降低延遲空間
• 暫存器指令相依性懲罰
– Load/Store可以自己排
– 相依性懲罰交給編譯器
關於inline
• 除了gcc的aways inline會成功,其他都有機會
失敗
• Clang 看程式碼行數,需要dump assembly檢
查
向量暫存器優化
• 現代編譯器無法有效的對特定演算法SIMD
優化,主因為90%的演算法需要用到大量
shuffle指令,大部分都紙筆算過一遍進行優
化
• 目前編譯器優化只到for loop unrolling 程度
for(int i=0;i<64;i++)
{
….
}
編譯器表示:
我知道怎麼優了
提取元素及寫回
reg128 reg0;
float4 a= {0,1,2,3};
reg0._float4 = a;
float2 val1= reg0._float2[0];
reg0._float2[1]=val1;
float val0=reg0._float[2];
reg0._float[3] = val0;
1. 指令是否支援,沒支援,寫入L1 Load/Store被當陣列用
2. 要看編譯器聰不聰明!!
0 1 2 3
寫
讀
讀
寫
寫
Dump assembly重要性
看有沒有超過32個暫存器
SIMD的第五步
極限優化方法論
• 參數固定
• 全部破壞,一路到底
• 拆除分支指令code會變非常龐大,速度快
• 別懷疑,程式隨便就超過4000行
SIMD優化觀念
FunctionA
演算法A
FunctionB
演算法B
FunctionC
演算法C
FunctionEnd
(最終演算法)
開發
一個月
之前做的
全部砍掉
只需開發
一個月
前面都白費
浪費時間
開發
一個月
開發
一個月
每天面臨的課題
關於資料
• 必須要滿足4的倍數的巨量資料
• 資料最大數量必須已知
• input資料重排,可以飛天
• 未滿足四的倍數
– 照樣SIMD餘數補0
– 末端改用純量暫存器
資料重排
a b a b a b ...
a b a b a b ...
a b a b a b ...
a b a b a b ...
a b a b a b ...
a b a b a b ...
a a a a a a ...
a a a a a a ...
a a a a a a ...
b b b b b b ...
b b b b b b ...
b b b b b b ...
a b c a b c ...
a b c a b c ...
a b c a b c ...
a b c a b c ...
a b c a b c ...
a b c a b c ...
a a a a a a ...
a a a a a a ...
b b b b b b ...
b b b b b b ...
c c c c c c ...
c c c c c c ...
手動unrolling
Image
傳統法
for(int i=0;i<height;i++)
{
for(int j=0;j<width;j++)
{
if(...) //上
else if(...) //下
else if(...) // 左
else if(...) //右
// 中
}
}
SIMD法
for(int i=0;i<height;i++) //上
{ ...}
for(int i=1;i<height-1;i++)
{
//左
...
for(int j=1;j<width-1;j++) // 中
{ ... }
//右
...
}
for(int i=0;i<height;i++) //下
{ ...}
為了配合SIMD,瘋狂,無限制的
unrolling
I cache 夠用(32Kb以上),
不夠再說吧
結論
• SIMD跟數學強連結
• 未知的領域,或是說大家都沒有上過課??
• 網路上資料極少,排成功的人也不多
• 想發明新演算法嗎?你可以玩玩看
Resource link
• https://plus.google.com/u/0/1071942202537
12101874

快快樂樂SIMD

Editor's Notes

  • #14 模擬暫存器行為,組合語言階層是沒有型別的