1. 程式人生 > >SIMD、SSE、AVX指令集

SIMD、SSE、AVX指令集

指令集

指令集是指CPU能執行的所有指令的集合,每一指令對應一種操作,任何程式最終要編譯成一條條指令才能讓CPU識別並執行。CPU依靠指令來計算和控制系統,所以指令強弱是衡量CPU效能的重要指標,指令集也成為提高CPU效率的有效工具。

CPU都有一個基本的指令集,比如說目前英特爾和AMD的絕大部分處理器都使用的是X86指令集,因為它們都源自於X86架構。但無論CPU有多快,X86指令也只能一次處理一個數據,這樣效率就很低下,畢竟在很多應用中,資料都是成組出現的,比如一個點的座標(XYZ)和顏色(RGB)、多聲道音訊等。為了提高CPU在某些方面的效能,就必須增加一些特殊的指令滿足時代進步的需求,這些新增的指令就構成了擴充套件指令集。該指令集採用單指令多資料(single instruction multiple data,簡稱 SIMD)擴充套件技術。

Intel擴充套件指令集的演變

Intel擴充套件指令集連結:intel擴充套件指令集機票

MMX(SIMD)

英特爾在1996年率先引入了MMX(Multi Media eXtensions)多媒體擴充套件指令集,也開創了SIMD(Single Instruction Multiple Data,單指令多資料)指令集之先河,即在一個週期內一個指令可以完成多個數據操作,MMX指令集的出現讓當時的MMX Pentium大出風頭。

SSE

SSE(Streaming SIMD Extensions,流式單指令多資料擴充套件)指令集是1999年英特爾在Pentium III處理器中率先推出的,並將向量處理能力從64位擴充套件到了128位。在Willamette核心的Pentium 4中英特爾又將擴充套件指令集升級到SSE2(2000年),而SSE3指令集(2004年)是從Prescott核心的Pentium 4開始出現。
SSE4(2007年)指令集是自SSE以來最大的一次指令集擴充套件,它實際上分成Penryn中出現的SSE4.1和Nehalem中出現的SSE4.2,其中SSE4.1佔據了大部分的指令,共有47條,Nehalem中的SSE4指令集更新很少,只有7條指令,這樣一共有54條指令,稱為SSE4.2。

AVX

2007年8月,AMD搶先宣佈了SSE5指令集(SSE到SSE4均為英特爾出品),英特爾當即黑臉表示不支援SSE5,轉而在2008年3月宣佈Sandy Bridge微架構將引入全新的AVX指令集,同年4月英特爾公佈AVX指令集規範,隨後開始不斷進行更新,業界普遍認為支援AVX指令集是Sandy Bridge最重要的進步,沒有之一。
AVX(Advanced Vector Extensions,高階向量擴充套件)指令集借鑑了一些AMD SSE5的設計思路,進行擴充套件和加強,形成一套新一代的完整SIMD指令集規範。

MMX(SIMD)、SSE、AVX

MMX

MMX系列指令集使用單獨的64bit暫存器(MM暫存器),暫存器個數不清楚,一次處理64bit的資料。可以存放資料如下。

暫存器結構:

MM0
MM1
MM2
MM3
MM4

每個MM暫存器可以儲存的值的大小和個數如下(bit, 1位元組(B)= 8bit)
MMX指令只能處理整型(字元,短整,整型,這裡的整形為32bit)
暫存器大小:

64bit

一次處理兩個32bit整型

32bit 32bit

一次處理4個16bit整型

16bit 16bit 16bit 16bit

一次處理8個字元

8bit 8bit 8bit 8bit 8bit 8bit 8bit 8bit

SSE

MMX系列指令集使用單獨的128bit暫存器(XMM暫存器),暫存器個數16(不同計算機可能不同),一次處理128bit的資料。可以存放資料如下。
暫存器結構:

XMM0
XMM1
XMM2
XMM3
XMM4

每個XMM暫存器可以儲存的值的大小和個數如下
SSE指令能處理整型,單精度浮點,雙精度浮點
注:為什麼會有16bit?拿整型來說,對於不同的計算機,佔的位元組不同,有一個size_t的資料型別,在不同計算機上可能佔得位元組不同(4B,或者2B),具體可以使用sizeof(資料型別)來檢視此型別佔得位元組數。
暫存器大小:

128bit

一次處理2個64bit的資料型別

64bit 64bit

一次處理4個32bit的資料型別

32bit 32bit 32bit 32bit

一次處理8個16bit資料型別

16bit 16bit 16bit 16bit 16bit 16bit 16bit 16bit

AVX

AVX高階向量擴充套件,在SSE的基礎上又把暫存器大小擴充套件為256bit。這次AVX將所有16個128位XMM暫存器擴充為256位的YMM暫存器,從而支援256位的向量計算。理想狀態下,浮點效能最高能達到前代的2倍水平。同時所有的SSE/SSE2/SSE3/SSSE3/SSE4指令是被AVX全面相容的(AVX不相容MMX),因此實際操作的是YMM暫存器的低128位,在這一點上與原來的SSE系列指令集無異。

  • 支援256位向量計算,浮點效能最大提升2倍

  • 增強的資料重排,更有效存取資料

  • 支援3運算元和4運算元,在向量和標量程式碼中能更好使用暫存器

  • 支援靈活的不對齊記憶體地址訪問

  • 支援靈活的擴充套件性強的VEX編碼方式,可減少程式碼

暫存器結構:

YMM0
YMM1
YMM2
YMM3
YMM4

暫存器大小

256bit

一次處理4個64bit的資料型別

64bit 64bit 64bit 64bit

一次處理8個32bit的資料型別

32bit 32bit 32bit 32bit 32bit 32bit 32bit 32bit

指令的使用

指令功能介紹

這裡只講解指令的簡單使用,內部原理及其定址什麼的流程,自行深拋。
程式必然包括資料和操作,要計算資料,肯定要去cpu的運算器,所以資料會從(如果資料小可能直接全放cache了)記憶體到cache,再從cache到register,然後進入運算器計算,計算得到資料如果短時間不用,或者想長久儲存,則可能需要重寫回記憶體或者硬碟。
簡單來說,分三步:

  • 寫入資料
  • 運算資料
  • 資料寫出
    那麼資料的寫入和寫出就對應了擴充套件指令集的訪存指令,資料計算就對應了運算指令。另外還有許多指令型別。具體如下:
    在這裡插入圖片描述

指令導讀

指令的具體用法還是要查指令手冊
每一個指令的構成都可以理解一個函式。
返回值型別 函式名 (形參列表)
1)型別

  • __m128 128bit 儲存單精度浮點float
    __m128i 128bit 儲存整形int
    __m128d 128bit 儲存雙精度浮點double
  • __m256 256bit 儲存單精度float
    __m256i 256bit 儲存整型int
    __m256d 256bit 儲存雙精度double
    2)函式名
_mm :128bit _mm256 :256bit
_load :操作 _load :操作
_ps :p=package,s=float _ps :p=package,s=float
_pd :p=package,s=double _pd :p=package,s=double
_ss :p=scalar,s=float _ss :p=scalar,s=float

package:是向量資料打包的意思 scalar是標量,一個數據的意思
packagescalar
在這裡插入圖片描述

3)形參:要傳入的資料的型別指定,一般為地址
如下圖:
函式
標頭檔案
指令
編譯Flags
Op:指令資料的操作規則
op操作規則

SSE,AVX為什麼會提升效能

SIMD

SIMD

SSE指令的不同形式

  • 垂直計算形式
    例如:_mm_add_ps();
    兩向量垂直操作
  • 水平計算形式
    例如:_mm_hadd_ps();
    _mm256_hadd_ps();
    128bit和256bit水平操作
  • 標量形式(scalar)
    scalar(標量)操作

示例程式碼

1-loop.c

#include<stdio.h>
#include <x86intrin.h>
#define N 128

int main(){
  	double a[N][N],b[N][N],c[N][N];
  	int i,j;

  	for(i=0;i<N;i++)
    		for(j=0;j<N;j++)
			{
     			 a[i][j]=10;
     			 b[i][j]=6;
    		}
/*
	for(i = 0; i < N; i++){
		for(j = 0; j < N; j++)
			c[i][j] = a[i][j] + b[i][j];
	}
*/
	int block = N / 4;
	int reserve = N % 4;
	__m256d ymm0, ymm1;
	__m256d avx_sum0 = _mm256_setzero_pd();
	for(i = 0; i < N; i++){
		for(j = 0; j < block; j ++){
			ymm0 = _mm256_loadu_pd(&a[i][j*4]);
			ymm1 = _mm256_loadu_pd(&b[i][j*4]);
			avx_sum0 = _mm256_add_pd(ymm0, ymm1);
			
			_mm256_storeu_pd(*(c+i)+j*4, avx_sum0);
		}
	}
 	if(c[4][6]==16)
		printf("\n結果正確,測試完成!\n\n");
	else
    		
		printf("\n結果不正確,測試完成!\n\n");
  	return 0;
}

2-ctrl_flow-f4

#include<stdio.h>
#include <x86intrin.h>
#define N 128
int main(){
	float a[N]={0};
  	float b[N]={1.1,2.2,3.3};
  	float c[N]={1,2,3};
  	int i, j;
	/*
  	for(i=0;i<N;i++){
    		if(b[i]<c[i])
      			a[i]=b[i]+a[i];
    	else
      		a[i]=c[i]-a[i];
  	}
	*/

	int block = N / 8;
	__m256 ymm_a = _mm256_setzero_ps(); 
	__m256 ymm_b = _mm256_setzero_ps(); 
	__m256 ymm_c = _mm256_setzero_ps();
	__m256 avx_sum, avx_sub;
	__m256 mask;
	__m256 blendv;
	for(i = 0; i < 1; i++){
		ymm_a = _mm256_loadu_ps(a +i*8);
		ymm_b = _mm256_loadu_ps(b +i*8);
		ymm_c = _mm256_loadu_ps(c +i*8);
		
		avx_sum = _mm256_add_ps(ymm_b, ymm_c);
		avx_sub = _mm256_sub_ps(ymm_c, ymm_a);
		mask = _mm256_cmp_ps(ymm_b,ymm_c, 2); //30

		blendv = _mm256_blendv_ps(avx_sub, avx_sum, mask);

		printf("%f, %f, %f, %f, %f, %f, %f, %f\n", 
			blendv[0], blendv[1], blendv[2], blendv[3],
			blendv[4], blendv[5], blendv[6], blendv[7]);
		_mm256_storeu_ps(a + i*8, blendv);
		
	}

  	if(a[2]==3)
    	printf("\n結果正確,測試完成!\n\n");
  	else
    	printf("\n結果不正確,測試完成!\n\n");
  	return 0;
}

3-reducetion-d4.c

#include<stdio.h>
#define N 128
#include <x86intrin.h>


int main()
{
	float sum = 1;
    	float a[N]={1,2,3};
    	int i;
	/*
    	for(i=0;i<N;i++)
        	sum+=a[i];
	*/
	__m256 avx_sum = _mm256_setzero_ps();
	__m256 ymm0;
	int block = N / 8;
	
	for(i = 0; i < block; i++){
		ymm0 = _mm256_loadu_ps(a + i*8);
		avx_sum = _mm256_hadd_ps(ymm0, avx_sum);
	}
	
	avx_sum = _mm256_hadd_ps(avx_sum, avx_sum);
	avx_sum = _mm256_hadd_ps(avx_sum, avx_sum);
	
	sum += avx_sum[0] + avx_sum[4];

	printf("sum = %f ", sum);
	if(sum==7)
        	printf("\n結果正確,測試完成!\n\n");
	else
       		printf("\n結果不正確,測試完成!\n\n");
    
    return 0;
}

4-unalign-d4.c

#include<stdio.h>
#define N 128
#include <x86intrin.h>
int main()
{
 	double a[N],b[N],c[N];

  	int i;

  	for(i=0;i<N;i++)
    		a[i]=3;
  	for(i=0;i<N;i++)
    		b[i]=4;
	/*
  	for(i=0;i<N/4(32);i++)
    		c[i]=a[i+1]+b[i+2];
	*/
	
	int block = N / 4 / 4;
	__m256d avx_sum0 = _mm256_setzero_pd();
	__m256d ymm0, ymm1;
	for(i = 0; i < block; i++){
		ymm0 = _mm256_loadu_pd(a + 1 +i*4);
		ymm1 = _mm256_loadu_pd(b + 1 +i*4);
		
		avx_sum0 = _mm256_add_pd(avx_sum0, ymm0);
		avx_sum0 = _mm256_add_pd(avx_sum0, ymm1);
		_mm256_storeu_pd(c + i*4, avx_sum0);
		avx_sum0 = _mm256_setzero_pd();
	}

  	if(c[2]==7)
    		printf("\n結果正確,測試完成!\n\n");
  	else
    		printf("\n結果不正確,測試完成!\n\n");
  	return 0;
}

5-cvt-df.c

#include<stdio.h>
#include <x86intrin.h>
#define N 128
int main(){
 	float a[N]={0};
  	double b[N]={1,2,3};
  	int i;
	
	/*
  	for(i=0;i<N;i++)
    		a[i]=b[i];
	*/

	int block = N / 4;
	__m256d ymm_pd;
	__m128 ymm_ps;
	for(i = 0; i < block; i++){
		ymm_pd = _mm256_loadu_pd(b + i*4);
		ymm_ps = _mm256_cvtpd_ps(ymm_pd);
		//printf("%f, %f, %f, %f\n", 
		//		ymm_ps[0], ymm_ps[1], ymm_ps[2], ymm_ps[3]);
		_mm_storeu_ps(a + i*4,ymm_ps);
	}
  	if(a[2]==3)  
    		printf("\n結果正確,測試完成!\n\n");
  	else
    		printf("\n結果不正確,測試完成!\n\n");
  	return 0;
}