SIMD指令集 SSE/AVX
概述
參考手冊
- Intel® Intrinsics Guide
- Tommesani.com Docs
- Intel® 64 and IA-32 Architectures Software Developer Manuals
背景
1. 什麼是指令集
所謂指令集,就是CPU中用來計算和控制計算機系統的一套指令的集合,而每一種新型的CPU在設計時就規定了一系列與其他硬體電路相配合的指令系統。而指令集的先進與否,也關系到CPU的性能發揮,它也是CPU性能展現的一個重要标志。
通俗的了解,指令集就是CPU能認識的語言,指令集運作于一定的微架構之上,不同的微架構可以支援相同的指令集,比如Intel和AMD的CPU的微架構是不同的,但是同樣支援X86指令集,這很容易了解,指令集隻是一套指令集合,一套指令規範,具體的實作,仍然依賴于CPU的翻譯和執行。指令集一般分為RISC(精簡指令集 Reduced Instruction Set Computer)和CISC(複雜指令集Complex Instruction Set Computer)。Intel X86的第一個CPU定義了第一套指令集,後來一些公司發現很多指令并不常用,是以決定設計一套簡潔高效的指令集,稱之為RICS指令集,進而将原來的Intel X86指令集定義為CISC指令集。兩者的使用場合不一樣,對于複雜的系統,CISC更合适,否則,RICS更合适,且低功耗。
SIMD(Single Instruction Multiple Data)指令集,指單指令多資料流技術,可用一組指令對多組資料通進行并行操作。SIMD指令可以在一個控制器上控制同時多個平行的處理微元,一次指令運算執行多個資料流,這樣在很多時候可以提高程式的運算速度。SIMD指令在本質上非常類似一個向量處理器,可對控制器上的一組資料(又稱“資料向量”) 同時分别執行相同的操作進而實作空間上的并行。SIMD是CPU實作DLP(Data Level Parallelism)的關鍵,DLP就是按照SIMD模式完成計算的。SSE和較早的MMX和 AMD的3DNow!都是SIMD指令集。它可以通過單指令多資料技術和單時鐘周期并行處理多個浮點來有效地提高浮點運算速度。
3. 指令集發展
指令集是一直在發展的,在CISC指令集中,慢慢的發展了一系列的指令集:
-
X86指令集:
X86指令集是Intel為其第一塊16位CPU(i8086)專門開發的,IBM1981年推出的世界第一台PC機中的CPU—i8088(i8086簡化版)使用的也是X86指令,同時電腦中為提高浮點資料處理能力而增加的X87晶片系列數學協處理器則另外使用X87指令,以後就将X86指令集和X87指令集統稱為X86指令集。
-
MMX指令集:(MultiMedia eXtensions)
1997年Intel公司推出了多媒體擴充指令集MMX,它包括57條多媒體指令。MMX指令主要用于增強CPU對多媒體資訊的處理能力,提高CPU處理3D圖形、視訊和音頻資訊的能力。
-
SSE指令集:Streaming SIMD Extensions(系列)
由于MMX指令并沒有帶來3D遊戲性能的顯著提升,是以,1999年Inter公司在Pentium III CPU産品中推出了資料流單指令序列擴充指令(SSE),相容MMX指令。SSE為Streaming SIMD Extensions的縮寫,如同其名稱所表示的,是一種SSE指令包括了四個主要的部份:單精确度浮點數運算指令、整數運算指令(此為MMX之延伸,并和MMX使用同樣的寄存器)、Cache控制指令、和狀态控制指令。
在Pentium 4 CPU中,Inter公司開發了新指令集SSE2。SSE2指令一共144條,包括浮點SIMD指令、整形SIMD指令、SIMD浮點和整形資料之間轉換、資料在MMX寄存器中轉換等幾大部分。其中重要的改進包括引入新的資料格式,如:128位SIMD整數運算和64位雙精度浮點運算等。
相對于SSE2,SSE3又新增加了13條新指令,此前它們被統稱為pni(prescott new instructions)。13條指令中,一條用于視訊解碼,兩條用于線程同步,其餘用于複雜的數學運算、浮點到整數轉換和SIMD浮點運算。
SSE4增加了50條新的增加性能的指令,這些指令有助于編譯、媒體、字元/文本處理和程式指向加速。
-
AVX指令集:(Advanced Vector Extensions)
在2010年AVX将之前浮點運算資料的寬度從128bit的擴充到256bit。同時新的CPU架構下資料傳輸速度也獲得了提升。AVX指令集在SIMD計算性能增強的同時也沿用了的MMX/SSE指令集。不過和MMX/SSE的不同點在于,增強的AVX指令在指令的格式上也發生了很大的變化。x86(IA-32/Intel 64)架構的基礎上增加了prefix(Prefix),是以實作了新的指令,也使更加複雜的指令得以實作,進而提升了x86 CPU的性能。AVX并不是x86 CPU的擴充指令集,可以實作更高的效率,同時和CPU硬體相容性也更好,在SSE指令的基礎上AVX也使SSE指令接口更加易用。
在2011年釋出的AVX2則在此基礎上加入了以下新内容:整數SIMD指令擴充至256位,2個新FMA單元及浮點FMA指令,離散資料加載指令“gather”,新的位移和廣播指令。
AVX-512 是 Intel 公司在 2013 年釋出的一套擴充指令集,其指令寬度擴充為 512 bit,每個時鐘周期内可執行 32 次雙精度或 64 次單精度浮點(FP)運算,專門針對圖像/音視訊處理、資料分析、科學計算、資料加密和壓縮和深度學習等大規模運算需求的應用場景。
MMX 64位整型 SSE 128位浮點運算,整數運算仍然要使用MMX 寄存器,隻支援單精度浮點運算 SSE2 對整型資料的支援,支援雙精度浮點數運算,CPU快取的控制指令 SSE3 擴充的指令包含寄存器的局部位之間的運算,例如高位和低位之間的加減運算;浮點數到整數的轉換,以及對超線程技術的支援。 SSE4 AVX 256位浮點運算 AVX2 對256位整型資料的支援,三運算指令(3-Operand Instructions) AVX512 512位運算
可以看到,CISC指令集是一隻在不斷發展的,随着需求的不斷增加,指令集也在不斷擴充,進而提高CPU的性能。使用軟體CPU-Z可以檢視CPU支援的指令集。

寄存器與指令資料細節
在MMX指令集中,使用的寄存器稱作MM0到MM7,實際上借用了浮點處理器的8個寄存器的低64Bit,這樣導緻了浮點運算速度降低。
SSE指令集推出時,Intel公司在Pentium III CPU中增加了8個128位的SSE指令專用寄存器,稱作XMM0到XMM7。這樣SSE指令寄存器可以全速運作,保證了與浮點運算的并行性。這些XMM寄存器用于4個單精度浮點數運算的SIMD執行,并可以與MMX整數運算或x87浮點運算混合執行。
2001年在Pentium 4上引入了SSE2技術,進一步擴充了指令集,使得XMM寄存器上可以執行8/16/32位寬的整數SIMD運算或雙精度浮點數的SIMD運算。對整型資料的支援使得所有的MMX指令都是多餘的了,同時也避免了占用浮點數寄存器。SSE2為了更好地利用高速寄存器,還新增加了幾條寄存指令,允許程式員控制已經寄存過的資料。這使得 SIMD技術基本完善。
SSE3指令集擴充的指令包含寄存器的局部位之間的運算,例如高位和低位之間的加減運算;浮點數到整數的轉換,以及對超線程技術的支援。
AVX是Intel的SSE延伸架構,把寄存器XMM 128bit提升至YMM 256bit,以增加一倍的運算效率。此架構支援了三運算指令(3-Operand Instructions),減少在編碼上需要先複制才能運算的動作。在微碼部分使用了LES LDS這兩少用的指令作為延伸指令Prefix。AVX的256bit的YMM寄存器分為兩個128bit的lanes,AVX指令并不支援跨lanes的操作。其中YMM寄存器的低128位與Intel SSE指令集的128bitXMM寄存器複用。盡管VGX并不要求記憶體對齊,但是記憶體對齊有助于提升性能。如對于128-bit通路的16位元組對齊和對于256-bit通路的32位元組對齊。
AVX雖然已經将支援的SIMD資料寬度增加到了256位,但僅僅增加了對256位的浮點SIMD支援,整點SIMD資料的寬度還停留在128位上,AVX2支援的整點SIMD資料寬度從128位擴充到256位。同時支援了跨lanes操作,加入了增強廣播、置換指令支援的資料元素類型、移位操作對各個資料元素可變移位數的支援、跨距訪存支援。AVX硬體由16個256bitYMM寄存器(YMM0~YMM15)組成。
每一代的指令集都是對上一代相容的,支援上一代的指令,也可以使用上一代的寄存器,也就是說,AVX2也依然支援128位,64位的操作,也可以使用上一代的寄存器(當然,寄存器的硬體實作可能有差別)。AVX也對部分之前的指令接口進行了重構,是以可以在指令文檔中找到幾個處于不同代際有着相同功能調用接口卻不相同的函數。
另外,不同代際的指令不要混用,每次狀态切換将消耗 50-80 個時鐘周期,會拖慢程式的運作速度。
MMX | SSE | SSE2 | AVX | AVX2 | |
---|---|---|---|---|---|
寄存器 | MM0-MM7 | XMM0-XMM7 | XMM0-XMM7 | YMM0-YMM15 | YMM0-YMM15 |
浮點 | 128bit | 128bit | 256bit | 256bit | |
整型 | 64bit | 128bit | 128bit | 256bit |
資料結構
由于通常沒有内建的128bit和256bit資料類型,SIMD指令使用自己建構的資料類型,這些類型以union實作,這些資料類型可以稱作向量,一般來說,MMX指令是__m64 類型的資料,SSE是__m128類型的資料等等。
typedef union __declspec(intrin_type) _CRT_ALIGN(8) __m64
{
unsigned __int64 m64_u64;
float m64_f32[2];
__int8 m64_i8[8];
__int16 m64_i16[4];
__int32 m64_i32[2];
__int64 m64_i64;
unsigned __int8 m64_u8[8];
unsigned __int16 m64_u16[4];
unsigned __int32 m64_u32[2];
} __m64;
typedef union __declspec(intrin_type) _CRT_ALIGN(16) __m128 {
float m128_f32[4];
unsigned __int64 m128_u64[2];
__int8 m128_i8[16];
__int16 m128_i16[8];
__int32 m128_i32[4];
__int64 m128_i64[2];
unsigned __int8 m128_u8[16];
unsigned __int16 m128_u16[8];
unsigned __int32 m128_u32[4];
} __m128;
typedef union __declspec(intrin_type) _CRT_ALIGN(16) __m128i {
__int8 m128i_i8[16];
__int16 m128i_i16[8];
__int32 m128i_i32[4];
__int64 m128i_i64[2];
unsigned __int8 m128i_u8[16];
unsigned __int16 m128i_u16[8];
unsigned __int32 m128i_u32[4];
unsigned __int64 m128i_u64[2];
} __m128i;
typedef struct __declspec(intrin_type) _CRT_ALIGN(16) __m128d {
double m128d_f64[2];
} __m128d;
資料類型 | 描述 |
---|---|
__m128 | 包含4個float類型數字的向量 |
__m128d | 包含2個double類型數字的向量 |
__m128i | 包含若幹個整型數字的向量 |
__m256 | 包含8個float類型數字的向量 |
__m256d | 包含4個double類型數字的向量 |
__m256i | 包含若幹個整型數字的向量 |
- 每一種類型,從2個下劃線開頭,接一個m,然後是向量的位長度。
- 如果向量類型是以d結束的,那麼向量裡面是double類型的數字。如果沒有字尾,就代表向量隻包含float類型的數字。
- 整形的向量可以包含各種類型的整形數,例如char,short,unsigned long long。也就是說,__m256i可以包含32個char,16個short類型,8個int類型,4個long類型。這些整形數可以是有符号類型也可以是無符号類型
記憶體對齊
為了友善CPU用指令對記憶體進行通路,通常要求某種類型對象的位址必須是某個值K(通常是2、4或8)的倍數,如果一個變量的記憶體位址正好位于它長度的整數倍,我們就稱他是自然對齊的。不同長度的記憶體通路會用到不同的彙編指令,這種對齊限制簡化了形成處理器和存儲器系統之間接口的硬體設計,提高了記憶體的通路效率。
通常對于各種類型的對齊規則如下:
數組 :按照基本資料類型對齊,第一個對齊了後面的自然也就對齊了。
聯合 :按其包含的長度最大的資料類型對齊。
結構體: 結構體中每個資料類型都要對齊
對于SIMD的記憶體對齊是指__m128等union在記憶體中存儲時的存儲方式。然而由于結構記憶體對齊的規則略微複雜,我們以結構為例進行說明:
一般情況下,由于記憶體對齊的原因存儲多種類型資料的結構體所占的記憶體大小并非元素本身類型大小之和。對于自然對齊而言:
- 對于各成員變量來說,存放的起始位址相對于結構的起始位址的偏移量必須為該變量的類型所占用的位元組數的倍數,各成員變量在存放的時候根據在結構中出現的順序依次申請空間, 同時按照上面的對齊方式調整位置, 空缺的位元組自動填充。
- 對于整個結構體來說,為了確定結構的大小為結構的位元組邊界數(即該結構中占用最大的空間的類型的位元組數)的倍數,是以在為最後一個成員變量申請空間後,還會根據需要自動填充空缺的位元組。
是以一般我們在定義結構體時定義各元素的順序也會影響實際結構體在存儲時的整體大小,把大小相同或相近的元素放一起,可以減少結構體占用的記憶體空間。
除了自然對齊的記憶體大小,我們也可以設定自己需要的對齊大小,我們稱之為對齊系數,如果結構内最大類型的位元組數小于對齊系數,結構體記憶體大小應按最大元素大小對齊,如果最大元素大小超過對齊系數,應按對齊系數大小對齊。
對齊系數大小的設定可以使用下列方法:
#pragma pack (16)
使用預編譯器指令要求對齊。
#pragma pack()
恢複為預設對齊方式。
__attribute__ ((aligned (16)))//GCC要求對齊
__declspec(intrin_type) _CRT_ALIGN(16)//Microsoft Visual C++要求對齊
聯合的記憶體對齊方式與結構類似。
SIMD的指令中通常有對記憶體對齊的要求,例如,SSE中大部分指令要求位址是16bytes對齊的,以_mm_load_ps函數來說明,這個函數對應于SSE的loadps指令。
函數原型為:
extern __m128 _mm_load_ps(float const*_A);
可以看到,它的輸入是一個指向float的指針,傳回的就是一個__m128類型的資料,從函數的角度了解,就是把一個float數組的四個元素依次讀取,傳回一個組合的__m128類型的SSE資料類型,進而可以使用這個傳回的結果傳遞給其它的SSE指令進行運算,比如加法等;從彙編的角度了解,它對應的就是讀取記憶體中連續四個位址的float資料,将其放入SSE的寄存器(XMM)中,進而給其他的指令準備好資料進行計算。其使用示例如下:
float input[4] = { 1.0f, 2.0f, 3.0f, 4.0f };
__m128 a = _mm_load_ps(input); //WARNING
這裡加載正确的前提是:input這個浮點數陣列都是對齊在16 bytes的邊上。否則程式會崩潰或得不到正确結果。如果沒有對齊,就需要使用_mm_loadu_ps函數,這個函數用于處理沒有對齊在16bytes上的資料,但是其速度會比較慢。
對于上面的例子,如果要将input指定為16bytes對齊,可以采用的方式是:
__declspec(align(16)) float input[4] = {1.0, 2.0, 3.0, 4.0};
為了簡化,頭檔案<xmmintrin.h>中定義了一個宏```_MM_ALIGN16```來表示上面的含義,即可以用:
```C
_MM_ALIGN16 float input[4] = {1.0, 2.0, 3.0, 4.0};
動态數組(dynamic array)可由_aligned_malloc函數為其配置設定空間:
input = (float*) _aligned_malloc(ARRAY_SIZE * sizeof(float), 16);
由
_aligned_malloc
函數配置設定空間的動态數組可以由
_aligned_free
函數釋放其占用的空間:
_aligned_free(input);
256-bit AVX 指令在記憶體通路上對記憶體對齊比128-bit SSE 指令有更高要求。雖然在一個cache-line 之内,Intel 的對齊和非對齊指令已經沒有性能差距了,但是由于AVX 有更長的記憶體通路寬度(YMM <-> memory),會更頻繁地觸及cache-line 邊界。是以1)盡量使用對齊記憶體配置設定;2)有時候記憶體對齊不能保證,可以用128-bit(XMM)指令通路記憶體,然後再組合成256-bit YMM
工作模式
packed和scalar
SIMD的運算指令分為兩大類:packed和scalar。
Packed指令是一次對XMM寄存器中的四個數(即DATA0 ~ DATA3)均進行計算,而scalar則隻對XMM寄存器中的DATA0進行計算。如下圖所示:
定址方式
SIMD指令和一般的x86 指令很類似,基本上包括兩種定址方式:寄存器-寄存器方式(reg-reg)和寄存器-記憶體方式(reg-mem):
addps xmm0, xmm1 ; reg-reg
addps xmm0, [ebx] ; reg-mem
大小端
float input[4] = { 1.0f, 2.0f, 3.0f, 4.0f };
__m128 a = _mm_load_ps(input);
由于x86的little-endian特性,位址較低的byte會放在寄存器的右邊。也就是說,在載入到XMM寄存器後,寄存器中的DATA0會是1.0,而DATA1是2.0,DATA2是3.0,DATA3是4.0。如果需要以相反的順序載入的話,可以用_mm_loadr_ps 這個intrinsic。
環境配置
使用軟體CPU-Z可以檢視CPU支援的指令集。
編譯器設定
我們可以在C/C++使用封裝的函數而不是嵌入的彙編代碼的方式來調用指令集,這就是Compiler Intrinsics。
Intrinsics指令是對MMX、SSE等指令集的指令的一種封裝,以函數的形式提供,使得程式員更容易編寫和使用這些進階指令,在編譯的時候,這些函數會被内聯為彙編,不會産生函數調用的開銷。
除了我們這裡使用的intrinsics指令,還有intrinsics函數需要以作區分,這兩者既有聯系又有差別。編譯器指令
#pragma intrinsic()
可以将一些指定的系統庫函數編譯為内部函數,進而去掉函數調用參數傳遞等的開銷,這種方式隻适用于編譯器規定的一部分函數,不是所有函數都能使用,同時會增大生成代碼的大小。
intrinsics更廣泛的使用是指令集的封裝,能将函數直接映射到進階指令集,同時隐藏了寄存器配置設定和排程等,進而使得程式員可以以函數調用的方式來實作彙編能達到的功能,編譯器會生成為對應的SSE等指令集彙編。
Intel Intrinsic Guide可以查詢到所有的Intrinsic指令、對應的彙編指令以及如何使用等。
對于VC來說,VC6支援MMX、3DNow!、SSE、SSE2,然後更高版本的VC支援更多的指令集。但是,VC沒有提供檢測Intrinsic函數集支援性的辦法。
而對于GCC來說,它使用-mmmx、-msse等編譯器開關來啟用各種指令集,同時定義了對應的
__MMX__
、
__SSE__
等宏,然後x86intrin.h會根據這些宏來聲明相應的Intrinsic函數集。
__MMX__
、
__SSE__
等宏可以幫助我們判斷Intrinsic函數集是否支援,但這隻是GCC的專用功能。
如果使用GCC編譯器時,使用intrinsics指令時需要在編寫cmake或者makefile檔案時加上相關參數,例如使用AVX指令集時添加
-mavx2
參數。
CMake
GCC:
頭檔案 | 宏 | 編譯器參數 |
---|---|---|
avx2intrin.h | | -mavx2 |
avxintrin.h | | -mavx |
emmintrin.h | | -msse2 |
nmmintrin.h | | -msse4.2 |
xmmintrin.h | | -msse |
mmintrin.h | | -mmmx |
頭檔案設定
#include <mmintrin.h> //MMX
#include <xmmintrin.h> //SSE(include mmintrin.h)
#include <emmintrin.h> //SSE2(include xmmintrin.h)
#include <pmmintrin.h> //SSE3(include emmintrin.h)
#include <tmmintrin.h>//SSSE3(include pmmintrin.h)
#include <smmintrin.h>//SSE4.1(include tmmintrin.h)
#include <nmmintrin.h>//SSE4.2(include smmintrin.h)
#include <wmmintrin.h>//AES(include nmmintrin.h)
#include <immintrin.h>//AVX(include wmmintrin.h)
#include <intrin.h>//(include immintrin.h)
上述頭檔案中,下一個頭檔案包含上一個頭檔案中内容,例如xmmintrin.h為SSE 頭檔案,此頭檔案裡包含MMX頭檔案,emmintrin.h為SSE2頭檔案,此頭檔案裡包含SSE頭檔案。
VC引入<intrin.h>會自動引入目前編譯器所支援的所有Intrinsic頭檔案。GCC引入<x86intrin.h>.
使用
使用方式
資料存取
使用SSE指令,首先要了解這一類用于進行初始化加載資料以及将寄存器的資料儲存到記憶體相關的指令,我們知道,大多數SSE指令是使用的xmm0到xmm8的寄存器,那麼使用之前,就需要将資料從記憶體加載到這些寄存器,在寄存器中完成運算後, 再把計算結果從寄存器中取出放入記憶體。C++程式設計人員在使用SSE指令函數程式設計時,除了加載存儲資料外,不必關心這些128位的寄存器的排程,你可以使用128位的資料類型__m128和一系列C++函數來實作這些算術和邏輯操作,而決定程式使用哪個SSE寄存器以及代碼優化是C++編譯器的任務。
load系列函數,用于加載資料,從記憶體到寄存器。
set系列函數,用于加載資料,大部分需要多個指令執行周期完成,但是可能不需要16位元組對齊.這一系列函數主要是類似于load的操作,但是可能會調用多條指令去完成,友善的是可能不需要考慮對齊的問題。
store系列函數,用于将計算結果等SSE寄存器的資料儲存到記憶體中。這一系列函數和load系列函數的功能對應,基本上都是一個反向的過程
SSE 指令和 AVX 指令混用
SSE/AVX 的混用有時不可避免,AVX-SSE transition penalty并不是由混合SSE和AVX指令導緻的,而是因為混合了legacy SSE encoding 和 VEX encoding。
是以在使用Intel intrinsic寫全新的程式時其實并不需要太擔心這個問題,因為隻要指定了合适的CPU 架構(比如-mavx),SSE 和AVX intrinsic 都會被編譯器生成VEX-encoding 代碼。
函數命名
SIMD指令的intrinsics函數名稱一般為如下形式,
_mm<bit_width>_<name>_<data_type>
-
表明了向量的位長度,即操作對象的資料類型大小,對于128位的向量,這個參數為空,對于256位的向量,這個參數為256。<bit_width>
-
描述了内聯函數的算術操作。一般由兩部分組成:<name>
- 第一部分是表示指令的作用,比如加法add等;
- 第二部分是可選的修飾符,表示一些特殊的作用,比如從記憶體對齊,逆序加載等;
-
表明了操作的粒度,具體情形見下表:<data_type>
<data_type>辨別 資料類型 epi8/epi16/epi32 有符号的8,16,32位整數 epu8/epu16/epu32 無符号的8,16.32位整數 si128/si256 未指定的128,256位向量 ps 包裝型單精度浮點數 ss scalar single precision floating point data 數量型單精度浮點數 pd pached double precision floating point data 包裝型雙精度浮點數 sd 數量型雙精度浮點數 可選的修飾符 示例 描述 u loadu Unaligned memory: 對記憶體未對齊的資料進行操作 s subs/adds Saturate: 飽和計算将考慮記憶體能夠存儲的最小/最大值。非飽和計算略記憶體問題。即計算的上溢和下溢 h hsub/hadd Horizontally: 在水準方向上做加減法 hi/lo mulhi 高/低位 r setr Reverse order: 逆序初始化向量 fm fmadd Fused-Multiply-Add(FMA)運算,單一指令進行三元運算
在飽和模式下,當計算結果發生溢出(上溢或下溢)時,CPU會自動去掉溢出的部分,使計算結果取該資料類型表示數值的上限值(如果上溢)或下限值(如果下溢)。
注釋中的printf部分是利用__m128這個資料類型來擷取相關的值,這個類型是一個union類型,具體定義可以參考相關頭檔案,但是,對于實際使用,有時候這個值是一個中間值,需要後面計算使用,就得使用store了,效率更高。上面使用的是_mm_loadu_ps和_mm_storeu_ps,不要求位元組對齊,如果使用_mm_load_ps和_mm_store_ps,會發現程式會崩潰或得不到正确結果。下面是指定位元組對齊後的一種實作方法:
這類函數名一般以__m開頭。函數名稱和指令名稱有一定的關系
部分函數的說明
_mm_load_ss用于scalar的加載,是以,加載一個單精度浮點數到寄存器的低位元組,其它三個位元組清0,(r0 := *p, r1 := r2 := r3 := 0.0)。
_mm_load_ps用于packed的加載(下面的都是用于packed的),要求p的位址是16位元組對齊,否則讀取的結果會出錯,(r0 := p[0], r1 := p[1], r2 := p[2], r3 := p[3])。
_mm_load1_ps表示将p位址的值,加載到寄存器的四個位元組,需要多條指令完成,是以,從性能考慮,在内層循環不要使用這類指令。(r0 := r1 := r2 := r3 := *p)。
_mm_loadh_pi和_mm_loadl_pi分别用于從兩個參數高底位元組等組合加載。具體參考手冊。
_mm_loadr_ps表示以_mm_load_ps反向的順序加載,需要多條指令完成,當然,也要求位址是16位元組對齊。(r0 := p[3], r1 := p[2], r2 := p[1], r3 := p[0])。
_mm_loadu_ps和_mm_load_ps一樣的加載,但是不要求位址是16位元組對齊,對應指令為movups。
_mm_set_ss對應于_mm_load_ss的功能,不需要位元組對齊,需要多條指令。(r0 = w, r1 = r2 = r3 = 0.0)
_mm_set_ps對應于_mm_load_ps的功能,參數是四個單獨的單精度浮點數,是以也不需要位元組對齊,需要多條指令。(r0=w, r1 = x, r2 = y, r3 = z,注意順序)
_mm_set1_ps對應于_mm_load1_ps的功能,不需要位元組對齊,需要多條指令。(r0 = r1 = r2 = r3 = w)
_mm_setzero_ps是清0操作,隻需要一條指令。(r0 = r1 = r2 = r3 = 0.0)
_mm_store_ss:一條指令,*p = a0
_mm_store_ps:一條指令,p[i] = a[i]。
_mm_store1_ps:多條指令,p[i] = a0。
_mm_storeh_pi,_mm_storel_pi:值儲存其高位或低位。
_mm_storer_ps:反向,多條指令。
_mm_storeu_ps:一條指令,p[i] = a[i],不要求16位元組對齊。
_mm_stream_ps:直接寫入記憶體,不改變cache的資料
permute
根據8位控制值從輸入向量中選擇元素
Shuffle
功能
跨距訪存支援即訪存時,每個SIMD資料的向量資料元素可以來自不相鄰的記憶體位址。AVX2的跨距訪存指令稱為”gather”指令,該指令的操作數是一個基位址加一個向量寄存器,向量寄存器中存放着SIMD資料中各個元素相對基位址的偏移量是多少。有了這條指令,CPU可以輕松用一條指令實作若幹不連續資料”聚集”到一個SIMD寄存器中。這會對編譯器和虛拟機充分利用向量指令帶來很大便利,尤其是自動向量化。另外,參考2中對跨距訪存指令的功能描述中可以看到,當該指令的偏移位址向量寄存器中任何兩個值相同時,都會出GP錯。這意味着編譯器還是需要些特殊處理才能利用好這條指令。
跨距訪存指令
但跨距訪存指令僅僅支援32位整點、64位整點、單精度浮點、雙精度浮點的跨距訪存操作。從參考4可以猜測其實gather指令隻是在硬體上分解成若幹條32位或64位的微訪存指令實作。這就移位着其實一條32×8的SIMD訪存其實就是8次32位普通資料訪存,其訪存延時和延時不确定性會非常大,聊剩于無。
引入了Fused-Multiply-Add(FMA)運算。所謂FMA,即可通過單一指令實作A = A ∗ B + C A=A*B+CA=A∗B+C計算。
FMA指令集是AVX的擴充指令集,即熔合乘法累積,一種三元運算指令,允許建立新的指令并有效率地執行各種複雜的運算。熔合乘法累積可結合乘法與加法運算,通過單一指令執行多次重複計算,進而簡化程式,進而使系統能快速執行繪圖、渲染、相片着色、立體音效,及複雜向量運算等計算量大的工作。
FMA則關系到浮點運算能力。Haswell架構中擁有2個新的FMA單元(Intel的FMA3指令),每個FMA單元支援8個單精度或4個雙精度浮點數,每周期單/雙精度FLOPs都要比AVX高1倍。
FMA擁有20種指令形式,與3種操作數次序組合,形成60種新指令,為選擇記憶體操作數或目的操作數提供了極大的靈活性。另外融合乘加還會自動選擇多項式的計算過程,降低了延遲。
在AVX中,Intel定義了兩個128位通道,分别是高通道和低通道,不同通道不能互取資料;到AVX2中,跨通道資料排列操作則實作了高低通道資料互通,效率更高。
引用
- CPU指令集介紹_。。。。-CSDN部落格
- 在C/C++代碼中使用SSE等指令集的指令(1)
- 在C/C++代碼中使用SSE等指令集的指令(3)
- 在C/C++代碼中使用SSE等指令集的指令(4)
- 在C/C++代碼中使用SSE等指令集的指令(5)
- x86 - Is NOT missing from SSE, AVX? - Stack Overflow
- Intel 内部指令 — AVX和AVX2學習筆記
- Intel 的AVX2指令集解讀
- 基于SSE指令集的程式設計簡介
- 單指令多資料SIMD的SSE/AVX指令集和API
- 為什麼AVX反而比SSE慢)
- SIMD技術中各向量化指令集的特性
- regatepagefirst_rank_v2rank_aggregation-4-114177421.pc_agg_rank_aggregation&utm_term=avx2+指令集&spm=1000.2123.3001.4430)
- 為什麼AVX反而比SSE慢)
- SIMD技術中各向量化指令集的特性