はじめに
前回は,ShuffleとPermuteの概略と,浮動小数点・整数共通の前半・後半のレーンの入れ替え命令について説明しました.
今回は,浮動小数点に関する,残りの命令を説明します.
float: __m256
__m256に対応する命令の説明です.
_mm256_shuffle_ps (AVX)
__m256 _mm256_shuffle_ps (__m256 a, __m256 b, const int imm8)
asm: vshufps ymm, ymm, ymm, imm8
CPI/Uops
Architecture | Latency | Throughput | Uops |
---|---|---|---|
Alderlake | 1 | 0.5 | - |
Icelake | 1 | 0.5 | 1 |
Skylake | 1 | 1 | 1 |
Broadwell | 1 | 1 | 1 |
Haswell | 1 | 1 | 1 |
Ivy Bridge | 1 | 1 | 1 |
Sandy Bridge | 1 | 1 | 1 |
Zen3 | 1 | 0.5 | 1 |
Zen2 | 1 | 0.5 | 1 |
Zen | 1 | 1 | 2 |
- UopsのサイトのみZenシリーズのレイテンシが3になっています.
- 公式ドキュメントは1,Agner Fogのデータも1,instlatx64のデータも1となっているため,1に修正しています.
- なお,スループット,Uopsはすべて同一です.
- AMDのCPU(Zen,Zen2,Zen3)はメモリからshuffleをしてもUopsが変わらず2,1,1です.
- IntelのCPUのメモリから読み込む場合のUopsは2で,マイクロフュージョンされません.
説明
128ビットの壁を超えず,imm8の引数に従って並び替えられ,要素a,bをインタリーブして出力します.
データは,8つの要素が,aabbaabbの順番で出力されます.
128ビット単位にして前半後半のデータに対して,SSE命令の_mm_shuffle_psを同時に実行する命令です.
Intelのshuffleのポートは,Skylakeまでは1つしかないためスループットが低いです.
一方AMDのZen2,3アーキテクチャは全て2つ備えているためIcelake以降のスループットと同等の性能があります.
以下に動作例を示します.
d[0:7] = {a a b b a a b b}
imm[0:1]:
d[0]= 0: a[0], 1: a[1],2: a[2], 3: a[3]
d[4]= 0: a[4], 1: a[5],2: a[6], 3: a[7]
imm[2:3]:
d[1]= 0: a[0], 1: a[1],2: a[2], 3: a[3]
d[5]= 0: a[4], 1: a[5],2: a[6], 3: a[7]
imm[4:5]:
d[2]= 0: b[0], 1: b[1],2: b[2], 3: b[3]
d[6]= 0: a[4], 1: a[5],2: a[6], 3: a[7]
imm[6:7]:
d[3]= 0: b[0], 1: b[1],2: b[2], 3: b[3]
d[7]= 0: a[4], 1: a[5],2: a[6], 3: a[7]
imm8は_MM_SHUFFLE
で指定すると便利です.
指定は逆順で,
出力の4/8番目,3/7番目,2/6番目,1/5番目の要素が,それぞれ入力の何番目の要素に対応しているか記述します.
#define _MM_SHUFFLE(fp3,fp2,fp1,fp0) \
(((fp3) << 6) | ((fp2) << 4) | ((fp1) << 2) | ((fp0)))
例えば,_MM_SHUFFLE(3,2,1,0)
で指定したらaabbaabbでインタリーブして出力します.
またビットで直接記述してもよいでしょう.
_MM_SHUFFLE(3,2,1,0)
は0b11100100
と書くこともできます.
いくつかのimm8を指定した例を示します.
_MM_SHUFFLE(3,2,1,0)
_MM_SHUFFLE(0,1,2,3)
_MM_SHUFFLE(0,0,0,0)
_MM_SHUFFLE(2,0,2,0)
いくつかの例を示すコードを例示します.
void test_shuffle_ps()
{
__m256 a, b, d;
a = _mm256_setr_ps(0, 1, 2, 3, 4, 5, 6, 7);
b = _mm256_setr_ps(10, 11, 12, 13, 14, 15, 16, 17);
print_m256(a);
print_m256(b);
printf("\n");
d = _mm256_shuffle_ps(a, b, _MM_SHUFFLE(3, 2, 1, 0));
print_m256(d);
d = _mm256_shuffle_ps(a, b, _MM_SHUFFLE(0, 0, 0, 0));
print_m256(d);
d = _mm256_shuffle_ps(a, b, _MM_SHUFFLE(2, 2, 1, 1));
print_m256(d);
}
出力
a: 0.00 1.00 2.00 3.00 | 4.00 5.00 6.00 7.00
b: 10.00 11.00 12.00 13.00 | 14.00 15.00 16.00 17.00
d: 0.00 1.00 12.00 13.00 | 4.00 5.00 16.00 17.00
d: 0.00 0.00 10.00 10.00 | 4.00 4.00 14.00 14.00
d: 1.00 1.00 12.00 12.00 | 5.00 5.00 16.00 16.00
_mm256_permute_ps (AVX)
__m256 _mm256_permute_ps (__m256 a, const int imm8)
asm: vpermilps ymm, ymm, imm8
CPI, Uops
Architecture | Latency | Throughput | Uops |
---|---|---|---|
Alderlake | 1 | 1 | - |
Icelake | 1 | 1 | 1 |
Skylake | 1 | 1 | 1 |
Broadwell | 1 | 1 | 1 |
Haswell | 1 | 1 | 1 |
Ivy Bridge | 1 | 1 | 1 |
Sandy Bridge | 1 | 1 | 1 |
Zen3 | 1 | 0.5 | 1 |
Zen2 | 1 | 0.5 | 1 |
Zen | 1 | 1 | 2 |
- UopsのサイトのみZenシリーズのレイテンシが3になっています.
- 公式ドキュメントは1,Agner Fogのデータも1,instlatx64のデータも1となっているため,1に修正しています.
- なお,スループット,Uopsはすべて同一です.
- AMDのCPU(Zen,Zen2,Zen3)はメモリからshuffleをしてもUopsが変わらず2,1,1です.
- IntelのCPUのメモリから読み込む場合のUopsは2で,マイクロフュージョンされません.
説明
shuffle_psの引数a,bに同一の物を入れた場合と全く同じ挙動をします.
同様に,MM_SHUFFLEにより移動先を指定します.
ただし,全く同じ回路を使っているわけではないため,一部でレイテンシが違います.
Icelakeではshuffleのほうが高速に動作します.
動作自体は,shuffle_psと全く同じ挙動です.
この命令は,次で説明するpermutevarと同じ命令名であり,どちらかというとpermutevarをするために用意された命令です.
この命令の説明は次で同時に例を示すため,ここでは詳細を割愛します.
_mm256_permutevar_ps (AVX)
_mm256_permutevar_ps (__m256 a, __m256i mask)
asm: vpermilps ymm, ymm, ymm
動作
_mm256_permutevar_ps
CPI/Uops
Architecture | Latency | Throughput | Uops |
---|---|---|---|
Alderlake | 1 | 1 | - |
Icelake | 1 | 1 | 1 |
Skylake | 1 | 1 | 1 |
Broadwell | 1 | 1 | 1 |
Haswell | 1 | 1 | 1 |
Ivy Bridge | 1 | 1 | 1 |
Sandy Bridge | 1 | 1 | 1 |
Zen3 | 3 | 0.5 | 1 |
Zen2 | 3 | 2 | 1 |
Zen | 4 | 4 | 2 |
- AMDのCPUはIntelのCPUよりもパフォーマンスが悪いです.
- UopsのサイトのみZenシリーズのレイテンシが5になっています.
- AMDのCPU(Zen,Zen2,Zen3)はメモリからshuffleをしてもUopsが変わらず2,1,1です.
- IntelのCPUのメモリから読み込む場合のUopsは2で,マイクロフュージョンされません.
説明
引数に即値imm8ではなく,レジスタマスクを取るpermuteです.
定数値imm8を使う場合は,コンパイル時に移動先が決まっている必要がある一方で,このレジスタマスクでの指定あれば,実行時に移動先を変更することができます.
命令の記号はvpermilpsで即値を使うpermute_psと同じです.
どちらの命令も128ビットの壁は超えられず移動可能な範囲は同じですが,即値の場合と異なり,こちらの命令は移動先を指定する変数が8つあり,前半と後半のレーンで並び順をかえられます.
_MM_SHUFFLE
に変わって_mm256_set_epi32
で8つの要素に並び順を0-3の値を入れることで指定することで前半と後半でそれぞれ並べ替えをすることができます.
128ビットレーンは超えられないため,0-7ではないことに注意してください.
下記に例を示します.
set_epi32に前半後半同一の並びを入れれば,permuteと同じ使い方となり,違う値を入れれば,前半後半で並び順をかえられます.
即値が要求されるpermute_psは変数を入れる動作はできませんが,マスクを入れるpermutevar_psは,実行時に値をかえても動作します.
void test_permute_ps()
{
__m256 a, d;
a = _mm256_setr_ps(0, 1, 2, 3, 4, 5, 6, 7);
print_m256(a);
printf("\n");
d = _mm256_permute_ps(a, _MM_SHUFFLE(3, 2, 1, 0));
//for (int i = 0; i < 4; i++) d = _mm256_permute_ps(a, _MM_SHUFFLE(0, 1, 2, i)); complile error
print_m256(d);
d = _mm256_permutevar_ps(a, _mm256_set_epi32(3, 2, 1, 0, 3, 2, 1, 0));
print_m256(d);
d = _mm256_permutevar_ps(a, _mm256_set_epi32(3, 2, 1, 0, 0, 1, 2, 3));
print_m256(d);
for (int i = 0; i < 4; i++)
{
d = _mm256_permutevar_ps(a, _mm256_set_epi32(3, 2, 1, 0, 0, 1, 2, i));
print_m256(d);
}
}
実行結果
a: 0.00 1.00 2.00 3.00 | 4.00 5.00 6.00 7.00
d: 0.00 1.00 2.00 3.00 | 4.00 5.00 6.00 7.00
d: 0.00 1.00 2.00 3.00 | 4.00 5.00 6.00 7.00
d: 3.00 2.00 1.00 0.00 | 4.00 5.00 6.00 7.00
d: 0.00 2.00 1.00 0.00 | 4.00 5.00 6.00 7.00
d: 1.00 2.00 1.00 0.00 | 4.00 5.00 6.00 7.00
d: 2.00 2.00 1.00 0.00 | 4.00 5.00 6.00 7.00
d: 3.00 2.00 1.00 0.00 | 4.00 5.00 6.00 7.00
_mm256_permutevar8x32_ps (AVX2)
__m256 _mm256_permutevar8x32_ps (__m256 a, __m256i idx)
asm: vpermps ymm, ymm, ymm
CPI, Uops
Architecture | Latency | Throughput | Uops |
---|---|---|---|
Alderlake | 3 | 1 | - |
Icelake | 3 | 1 | 1 |
Skylake | 3 | 1 | 1 |
Broadwell | 3 | 1 | 1 |
Haswell | 3 | 1 | 1 |
Zen3 | 8 | 1 | 2 |
Zen2 | 8 | 2 | 2 |
Zen | 5 | 4 | 3 |
- AIDA64のZen2,Zen3はL8.5
- ZenシリーズのUopsはメモリから読み込んでもマイクロフュージョンされず,それぞれ4,3,3になります
- 一方IntelはマイクロフュージョンされてUopsは2になります
説明
1つのレジスタの8個の要素を128ビットの壁を超えて任意に並べ替えます.
レイテンシは,超えないSWIZZLE命令よりも長くなっています.
permutevarのように_mm256_set_epi32
で8つの要素を指定しますが,値は0から7の8通りを取ることができます.
一方,permutevarは128ビットを超えられないため0から3まで指定することになります.
この命令は浮動小数点である__m256に対する命令ですが,AVX2からの対応になります.
void test_permutevar8x32_ps()
{
__m256 a, d;
a = _mm256_setr_ps(0, 1, 2, 3, 4, 5, 6, 7);
print_m256(a);
printf("\n");
d = _mm256_permutevar8x32_ps(a, _mm256_set_epi32(7, 6, 5, 4, 3, 2, 1, 0));
print_m256(d);
d = _mm256_permutevar8x32_ps(a, _mm256_set_epi32(0, 1, 2, 3, 4, 5, 6, 7));
print_m256(d);
d = _mm256_permutevar8x32_ps(a, _mm256_set_epi32(1, 2, 3, 4, 5, 6, 7, 0));
print_m256(d);
d = _mm256_permutevar8x32_ps(a, _mm256_set_epi32(0, 0, 0, 0, 0, 0, 0, 0));
print_m256(d);
}
a: 0.00 1.00 2.00 3.00 | 4.00 5.00 6.00 7.00
d: 0.00 1.00 2.00 3.00 | 4.00 5.00 6.00 7.00
d: 7.00 6.00 5.00 4.00 | 3.00 2.00 1.00 0.00
d: 0.00 7.00 6.00 5.00 | 4.00 3.00 2.00 1.00
d: 0.00 0.00 0.00 0.00 | 0.00 0.00 0.00 0.00
double: __m256d
__m256dに対応する命令の説明です.
_mm256_shuffle_pd (AVX)
__m256d _mm256_shuffle_pd (__m256 a, __m256d b, const int imm8)
asm: vshufpd ymm, ymm, ymm, imm8
CPI, Uops
Architecture | Latency | Throughput | Uops |
---|---|---|---|
Alderlake | 1 | 0.5 | - |
Icelake | 1 | 0.5 | 1 |
Skylake | 1 | 1 | 1 |
Broadwell | 1 | 1 | 1 |
Haswell | 1 | 1 | 1 |
Ivy Bridge | 1 | 1 | 1 |
Sandy Bridge | 1 | 1 | 1 |
Zen3 | 1 | 0.5 | 1 |
Zen2 | 1 | 0.5 | 1 |
Zen | 2 | 1 | 2 |
- Zen2,Zen3でメモリから読み込む場合はUopsが1,Zenでメモリから読み込む場合はUopsが2です.
- Intel CPUでメモリから読み込む場合はUopsは2です.
- uopsのZenシリーズのレイテンシは3ですが,他の情報源は1or2です.
説明
128ビットの壁は超えず,imm8の引数に従ってababとインタリーブします.
0なら前半,1なら後半を示します.
d[0:7] = {a b a b}
imm[0]:
d[0]= 0: a[0], 1: a[1]
imm[1]:
d[1]= 0: a[0], 1: a[1]
imm[2]:
d[2]= 0: b[0], 1: b[1]
imm[3]:
d[3]= 0: b[0], 1: b[1]
imm8はfloatの時の_MM_SHUFFLE
で指定するのではなく,2進数で指定すると便利です.
指定は逆順で,0bxxxx
の4桁を指定すれば良いです.
また,float時のshuffle_psと違って,前半後半で共通の並び順を指定しないといけないという制約はありません.
いくつかの例を持つコードを示します.
void test_shuffle_pd()
{
__m256d a, b, d;
a = _mm256_setr_pd(0, 1, 2, 3);
b = _mm256_setr_pd(10, 11, 12, 13);
print_m256d(a);
print_m256d(b);
printf("\n");
d = _mm256_shuffle_pd(a, b, 0b0000);
print_m256d(d);
d = _mm256_shuffle_pd(a, b, 0b1100);
print_m256d(d);
d = _mm256_shuffle_pd(a, b, 0b1111);
print_m256d(d);
d = _mm256_shuffle_pd(a, b, 0b1001);
print_m256d(d);
}
出力
a: 0.00 1.00 | 2.00 3.00
b: 10.00 11.00 | 12.00 13.00
d: 0.00 10.00 | 2.00 12.00
d: 0.00 10.00 | 3.00 13.00
d: 1.00 11.00 | 3.00 13.00
d: 1.00 10.00 | 2.00 13.00
_mm256_permute_pd (AVX)
__m256d _mm256_permute_pd (__m256d a, int imm8)
asm: vpermilpd ymm, ymm, imm8
CPI, Uops
Architecture | Latency | Throughput | Uops |
---|---|---|---|
Alderlake | 1 | 1 | - |
Icelake | 1 | 1 | 1 |
Skylake | 1 | 1 | 1 |
Broadwell | 1 | 1 | 1 |
Haswell | 1 | 1 | 1 |
Ivy Bridge | 1 | 1 | 1 |
Sandy Bridge | 1 | 1 | 1 |
Zen3 | 1 | 0.5 | 1 |
Zen2 | 1 | 0.5 | 1 |
Zen | 2 | 1 | 2 |
- uopsのZenシリーズのレイテンシは3ですが,他の情報源は1or2です.
- Zen2,Zen3でメモリから読み込む場合はUopsが1,Zenでメモリから読み込む場合はUopsが2です.
- Intel CPUでメモリから読み込む場合はUopsは2です.
説明
shuffle_pdの引数a,bに同一の物を入れた場合と全く同じ挙動をします.
ただし,同じ回路を使っているわけではないため,CPIが違う可能性があります.
データシートによるとIcelakeではshuffleのほうが高速に動作します.
Intel intrinsic guideでは-(ハイフンに),なってますが,Angerのデータによると1です.
shuffle_pdと全く同じ挙動であり,また,次のpermutevarの説明で同時に例を示すため,ここでは詳細を割愛します.
_mm256_permutevar_pd (AVX)
__m256d _mm256_permutevar_pd (__m256d a, __m256i b)
asm: vpermilpd ymm, ymm, ymm
CPI, Uops
Architecture | Latency | Throughput | Uops |
---|---|---|---|
Alderlake | 1 | 1 | - |
Icelake | 1 | 1 | 1 |
Skylake | 1 | 1 | 1 |
Broadwell | 1 | 1 | 1 |
Haswell | 1 | 1 | 1 |
Ivy Bridge | 1 | 1 | 1 |
Sandy Bridge | 1 | 1 | 1 |
Zen3 | 3 | 0.5 | 1 |
Zen2 | 3 | 2 | 1 |
Zen | 4 | 4 | 2 |
- uopsのZenシリーズのレイテンシは5ですが,他の情報源はこの表のとおりです.
- AMDのCPUはメモリから読み込んでもUopsが変化せず2,1,1です.
説明
引数に即値imm8ではなく,レジスタマスクをとるpermuteです.
imm8は即値が必要なため,コンパイル時に決まっている必要があります.
一方でレジスタマスクであれば,実行時に変更することができます.
アセンブラ命令の記号はvpermilpdで即値のpermuteと同じです.
相変わらず128ビットの壁は超えられません.
0bxxxx
に変わって_mm256_set_epi64x
で4つの要素に並び順を0か2の値(最下位より一つ上のビット)を入れることで指定することで前半と後半でそれぞれ並べ替えをすることができます.
1ではないので注意してください.
下記に例を示します.
void test_permute_pd()
{
__m256d a, d;
a = _mm256_setr_pd(0, 1, 2, 3);
print_m256d(a);
printf("\n");
d = _mm256_permute_pd(a, 0b1111);
print_m256d(d);
d = _mm256_permutevar_pd(a, _mm256_set_epi64x(2, 2, 2, 2));
print_m256d(d);
for (int i = 0; i < 2; i++)
{
d = _mm256_permutevar_pd(a, _mm256_setr_epi64x(2, 2, 2, 2*i));
print_m256d(d);
}
}
実行結果
a: 0.00 1.00 | 2.00 3.00
d: 1.00 1.00 | 3.00 3.00
d: 1.00 1.00 | 3.00 3.00
d: 1.00 1.00 | 3.00 2.00
d: 1.00 1.00 | 3.00 3.00
_mm256_permute4x64_pd (AVX2)
__m256d _mm256_permute4x64_pd (__m256d a, const int imm8)
asm: vpermpd ymm, ymm, ymm
CPI, Uops
Architecture | Latency | Throughput | Uops |
---|---|---|---|
Alderlake | 3 | 1 | - |
Icelake | 3 | 1 | 1 |
Skylake | 3 | 1 | 1 |
Broadwell | 3 | 1 | 1 |
Haswell | 3 | 1 | 1 |
Zen3 | 6.5 | 1.25 | 2 |
Zen2 | 6 | 1.25 | 2 |
Zen | 2 | 2 | 3 |
- uops Zen L:2 T: 1, Zen2 L:6 T: 1.27, Zen3 L:6 T: 1.25
- Agner Zen L2, T: 2, Zen2 L6, T: 1, Zen3 L6.5, T: 1
- AIDA64 Zen L2,T:2, Zen2 L6.1,T:1.25, Zen3 L6.5,T:1.17
- ZenシリーズのUopsは,メモリから読み込んでもへらず4,3,3となります.Intel同様に減りません(すべてマイクロフュージョンされません).
説明
1つのレジスタの4個の要素を128ビットの壁を超えて任意に並べ替えます.
レイテンシは,超えないSWIZZLE命令よりも長くなっています.
_mm256_permutevar8x32_ps
と違ってvarがついておらず,即値をしています.
_MM_SHUFFLE
を使って,並べ替えます.
SSEの__m128
に対するpermute_ps命令を,__m256
のレーン全体への命令に拡張した動きをします.
double演算ですがAVX2からの対応です.
もし,即値ではなく,レジスタマスクで__m256dの並び替え操作をしたい場合は,__m256dのデータを__m256が2つ連続して入ったデータと見なして,permutevar8x32をキャストして使うしかありません.
以下に使用例を示します.
なお,キャストして用いる場合は,_mm_shuffle4x64
という変換用のマクロ関数を用意することで並び替えを容易にしていますが,マスクの生成が実際のpermute命令よりもかかるためあまり効率的とは言えません.
inline __m256i _mm_shuffle4x64(int f3, int f2, int f1, int f0)
{
/*
const int f0_2 = f0 << 1;
const int f1_2 = f1 << 1;
const int f2_2 = f2 << 1;
const int f3_2 = f3 << 1;
return _mm256_set_epi32(f3_2 + 1, f3_2, f2_2 + 1, f2_2, f1_2 + 1, f1_2, f0_2 + 1, f0_2);
*/
__m256i ret = _mm256_slli_epi64(_mm256_set_epi64x(f3, f2, f1, f0),1);
__m256i ret2 = _mm256_shuffle_epi32(_mm256_add_epi64(ret, _mm256_set1_epi64x(1)), _MM_SHUFFLE(2,3,0,1));
ret = _mm256_add_epi32(ret, ret2);
return ret;
}
void test_permute4x64_pd()
{
__m256d a, d;
a = _mm256_setr_pd(0, 1, 2, 3);
print_m256d(a);
printf("\n");
d = _mm256_permute4x64_pd(a, _MM_SHUFFLE(0, 0, 0, 0));
print_m256d(d);
d = _mm256_permute4x64_pd(a, _MM_SHUFFLE(3, 2, 1, 0));
print_m256d(d);
d = _mm256_permute4x64_pd(a, _MM_SHUFFLE(1, 1, 1, 1));
print_m256d(d);
for (int i = 0; i < 4; i++)
{
d = *(__m256d*)(&_mm256_permutevar8x32_ps(*((__m256*) & a), _mm_shuffle4x64(3, 2, 1, i)));
print_m256d(d);
}
}
a: 0.00 1.00 | 2.00 3.00
d: 0.00 0.00 | 0.00 0.00
d: 0.00 1.00 | 2.00 3.00
d: 1.00 1.00 | 1.00 1.00
d: 0.00 1.00 | 2.00 3.00
d: 1.00 1.00 | 2.00 3.00
d: 2.00 1.00 | 2.00 3.00
d: 3.00 1.00 | 2.00 3.00
なお,psへの命令はありません.floatの8つのレジスタを2つづつに分けて考えて4ブロックをソートする場合は,整数命令か,double命令を使うためにキャストして使用してください.
また,AMDのCPUなどそもそもこの命令が遅い場合は,特定の並び替え命令向けにshuffle2回とpermute2f128命令を使うことで代用することができます.
ただし,このようにするとレイテンシは改善しますが,スループットは直接命令を発行したほうが速いため,多くの場合では速度は改善しません.
なお,並び順によってはshuffleを一つ削除可能です.
- permute4x64: Zen2 L:6.0, T: 1.25, Zen3 L:6.5, T: 1.25
- shuffle: L:1, T: 0.5 or L:1, T: 1
- permute2f128:L: 3.5, T: 1
- shffx2+perm2:L: 5.5, T: 2 or 5.5, T: 3
//for _MM_SHUFFLE(3, 1, 2, 0)
inline __m256 _mm256_permute4x64_ps(__m256 src, const int imm8)
{
//perm128x1, shuffle_pd x2
__m256 tmp = _mm256_permute2f128_ps(src, src, 0x01);
__m256d tm2 = _mm256_shuffle_pd(_mm256_castps_pd(src), _mm256_castps_pd(tmp), 0b1100);
__m256 ret = _mm256_castpd_ps(_mm256_shuffle_pd(tm2, tm2, 0b0110));
return ret;
}