打包和解交错两个 __m256 寄存器
Packing and de-interleaving two __m256 registers
我有一个按行排列的浮点数组(~20 列 x ~1M 行),我需要从中一次提取两列到两个 __m256
寄存器中。
...a0.........b0......
...a1.........b1......
// ...
...a7.........b7......
// end first __m256
一个天真的方法是
__m256i vindex = _mm256_setr_epi32(
0,
1 * stride,
2 * stride,
// ...
7 * stride);
__m256 colA = _mm256_i32gather_ps(baseAddrColA, vindex, sizeof(float));
__m256 colB = _mm256_i32gather_ps(baseAddrColB, vindex, sizeof(float));
但是,我想知道通过在一个 gather
中检索 a0, b0, a1, b1, a2, b2, a3, b3
和在另一个 a4, b4, ... a7, b7
中检索 a4, b4, ... a7, b7
是否会获得更好的性能,因为它们在内存中更接近,然后 de - 交错他们。即:
// __m256 lo = a0 b0 a1 b1 a2 b2 a3 b3 // load proximal elements
// __m256 hi = a4 b4 a5 b5 a6 b6 a7 b7
// __m256 colA = a0 a1 a2 a3 a4 a5 a6 a7 // goal
// __m256 colB = b0 b1 b2 b3 b4 b5 b6 b7
我不知道如何很好地交错 lo
和 hi
。我基本上需要 _mm256_unpacklo_ps
的对立面。我想出的最好的是:
__m256i idxA = _mm256_setr_epi32(0, 2, 4, 6, 1, 3, 5, 7);
__m256i idxB = _mm256_setr_epi32(1, 3, 5, 7, 0, 2, 4, 6);
__m256 permLA = _mm256_permutevar8x32_ps(lo, idxA); // a0 a1 a2 a3 b0 b1 b2 b3
__m256 permHB = _mm256_permutevar8x32_ps(hi, idxB); // b4 b5 b6 b7 a4 a5 a6 a7
__m256 colA = _mm256_blend_ps(permLA, permHB, 0b11110000); // a0 a1 a2 a3 a4 a5 a6 a7
__m256 colB = _mm256_setr_m128(
_mm256_extractf128_ps(permLA, 1),
_mm256_castps256_ps128(permHB)); // b0 b1 b2 b3 b4 b5 b6 b7
那是 13 个周期。有没有更好的方法?
(据我所知,预取已经在尽可能地优化朴素的方法,但由于缺乏这方面的知识,我希望对第二种方法进行基准测试。如果有人已经知道这会是什么结果,请分享一下。使用上面的去隔行扫描方法,它比原始方法慢了大约 8%。)
Edit 即使没有去隔行扫描,"proximal" gather 方法也比原始的、恒定步长的 gather 方法慢大约 6%。我认为这意味着这种访问模式混淆了硬件预取太多而不值得优化。
为了加载 32 位浮点类型的列,您可以使用内部函数 _mm256_setr_pd 和 _mm256_shuffle_ps(需要 10 个周期):
#include <iostream>
#include <immintrin.h>
inline void Print(const __m256 & v)
{
float b[8];
_mm256_storeu_ps(b, v);
for (int i = 0; i < 8; i++)
std::cout << b[i] << " ";
std::cout << std::endl;
}
int main()
{
const size_t stride = 100;
float m[stride * 8];
for (size_t i = 0; i < stride*8; ++i)
m[i] = (float)i;
const size_t stride2 = stride / 2;
double * p = (double*)m;
__m256 ab0145 = _mm256_castpd_ps(_mm256_setr_pd(p[0 * stride2], p[1 * stride2], p[4 * stride2], p[5 * stride2]));
__m256 ab2367 = _mm256_castpd_ps(_mm256_setr_pd(p[2 * stride2], p[3 * stride2], p[6 * stride2], p[7 * stride2]));
__m256 a = _mm256_shuffle_ps(ab0145, ab2367, 0x88);
__m256 b = _mm256_shuffle_ps(ab0145, ab2367, 0xDD);
Print(a);
Print(b);
return 0;
}
输出:
0 100 200 300 400 500 600 700
1 101 201 301 401 501 601 701
关于内部 _mm256_i32gather_ps 的性能,我建议查看 here。
我假设 a 和 b 被放置在 0,10,然后是 1,11 到 9,19,如果没有根据需要更改 vindexm[]
;
如果要使用聚集指令:
//#includes
#define Distance 20 // number of columns.
float a[32][20]__attribute__(( aligned(32)))= {{1.01,1.02,1.03,1.04,1.05,1.06,1.07,1.08,1.09,1.10,1.11,1.12,1.13,1.14,1.15,1.16},
{2.01,2.02,2.03,2.04,2.05,2.06,2.07,2.08,2.09,2.10,2.11,2.12,2.13,2.14,2.15,2.16},
{3.01,3.02,3.03,3.04,3.05,3.06,3.07,3.08,3.09,3.10,3.11,3.12,3.13,3.14,3.15,3.16},
{4.01,4.02,4.03,4.04,4.05,4.06,4.07,4.08,4.09,4.10,4.11,4.12,4.13,4.14,4.15,4.16},
{5.01,5.02,5.03,5.04,5.05,5.06,5.07,5.08,5.09,5.10,5.11,5.12,5.13,5.14,5.15,5.16},
{6.01,6.02,6.03,6.04,6.05,6.06,6.07,6.08,6.09,6.10,6.11,6.12,6.13,6.14,6.15,6.16},
{7.01,7.02,7.03,7.04,7.05,7.06,7.07,7.08,7.09,7.10,7.11,7.12,7.13,7.14,7.15,7.16},
{8.01,8.02,8.03,8.04,8.05,8.06,8.07,8.08,8.09,8.10,8.11,8.12,8.13,8.14,8.15,8.16},
{9.01,9.02,9.03,9.04,9.05,9.06,9.07,9.08,9.09,9.10,9.11,9.12,9.13,7.14,9.15,9.16},
{10.1,10.2,10.3,10.4,10.5,10.6,10.7,10.8,10.9,10.10,10.11,10.12,10.13,10.14,10.15,10.16},
{11.1,11.2,11.3,11.4,11.5,11.6,11.7,11.8,11.9,11.10,11.11,11.12,11.13,11.14,11.15,11.16},
{12.1,12.2,12.3,12.4,12.5,12.6,12.7,12.8,12.9,12.10,12.11,12.12,12.13,12.14,12.15,12.16},
{13.1,13.2,13.3,13.4,13.5,13.6,13.7,13.8,13.9,13.10,13.11,13.12,13.13,13.14,13.15,13.16},
{14.1,14.2,14.3,14.4,14.5,14.6,14.7,14.8,14.9,14.10,14.11,14.12,14.13,14.14,14.15,14.16},
{15.1,15.2,15.3,15.4,15.5,15.6,15.7,15.8,15.9,15.10,15.11,15.12,15.13,15.14,15.15,15.16},
{16.1,16.2,16.3,16.4,16.5,16.6,16.7,16.8,16.9,16.10,16.11,16.12,16.13,16.14,16.15,16.16}};
float tempps[8];
void printVecps(__m256 vec)
{
_mm256_store_ps(&tempps[0], vec);
printf(", [0]=%3.2f, [1]=%3.2f, [2]=%3.2f, [3]=%3.2f, [4]=%3.2f, [5]=%3.2f, [6]=%3.2f, [7]=%3.2f \n",
tempps[0],tempps[1],tempps[2],tempps[3],tempps[4],tempps[5],tempps[6],tempps[7]) ;
}
int main() {
__m256 vec1;
int vindexm [8]={0, Distance/2, Distance, Distance + Distance/2, Distance*2, Distance*2 +Distance/2, Distance*3, Distance*3 + Distance/2};
__m256i vindex = _mm256_load_si256((__m256i *) &vindexm[0]);
//loops
vec1 = _mm256_i32gather_ps (&a[0][0],vindex, 4);//place it in your loop as you want
printVecps(vec1);
return 0;
}
输出是
[0]=1.01, [1]=1.11, [2]=2.01, [3]=2.11, [4]=3.01, [5]=3.11, [6]=4.01, [7]=4.11
// __m256 lo = a0 b0 a1 b1 a2 b2 a3 b3 // load proximal elements
// __m256 hi = a4 b4 a5 b5 a6 b6 a7 b7
// __m256 colA = a0 a1 a2 a3 a4 a5 a6 a7 // goal
// __m256 colB = b0 b1 b2 b3 b4 b5 b6 b7
看来我们可以比我原来的答案更快地进行这种洗牌:
void unpack_cols(__m256i lo, __m256i hi, __m256i& colA, __m256i& colB) {
const __m256i mask = _mm256_setr_epi32(0, 2, 4, 6, 1, 3, 5, 7);
// group cols crossing lanes:
// a0 a1 a2 a3 b0 b1 b2 b3
// a4 a5 a6 a7 b4 b5 b6 b7
auto lo_grouped = _mm256_permutevar8x32_epi32(lo, mask);
auto hi_grouped = _mm256_permutevar8x32_epi32(hi, mask);
// swap lanes:
// a0 a1 a2 a3 a4 a5 a6 a7
// b0 b1 b2 b3 b4 b5 b6 b7
colA = _mm256_permute2x128_si256(lo_grouped, hi_grouped, 0 | (2 << 4));
colB = _mm256_permute2x128_si256(lo_grouped, hi_grouped, 1 | (3 << 4));
}
虽然两条指令在 Haswell 上都有 3 个周期的延迟(参见 Agner Fog) they have a single cycle throughput. This means it has a throughput of 4 cycles and 8 cycles latency. If you have a spare register which can keep the mask this should be better. Doing only two of these in parallel allows you to completly hide its latency. See godbolt and rextester。
旧答案,留作参考:
执行此随机播放的最快方法如下:
void unpack_cols(__m256i lo, __m256i hi, __m256i& colA, __m256i& colB) {
// group cols within lanes:
// a0 a1 b0 b1 a2 a3 b2 b3
// a4 a5 b4 b5 a6 a7 b6 b7
auto lo_shuffled = _mm256_shuffle_epi32(lo, _MM_SHUFFLE(3, 1, 2, 0));
auto hi_shuffled = _mm256_shuffle_epi32(hi, _MM_SHUFFLE(3, 1, 2, 0));
// unpack lo + hi a 64 bit
// a0 a1 a4 a5 a2 a3 a6 a7
// b0 b1 b4 b5 b2 b3 b6 b7
auto colA_shuffled = _mm256_unpacklo_epi64(lo_shuffled, hi_shuffled);
auto colB_shuffled = _mm256_unpackhi_epi64(lo_shuffled, hi_shuffled);
// swap crossing lanes:
// a0 a1 a2 a3 a4 a5 a6 a7
// b0 b1 b2 b3 b4 b5 b6 b7
colA = _mm256_permute4x64_epi64(colA_shuffled, _MM_SHUFFLE(3, 1, 2, 0));
colB = _mm256_permute4x64_epi64(colB_shuffled, _MM_SHUFFLE(3, 1, 2, 0));
}
从 Haswell 开始,它的吞吐量为 6 个周期(遗憾的是端口 5 上有 6 个指令)。根据 Agner Fog _mm256_permute4x64_epi64
有 3 个周期的延迟。这意味着 unpack_cols
的延迟为 11 8 个周期。
您可以查看 godbolt.org or test it at rextester 上的代码,它支持 AVX2,但遗憾的是没有像 godbolt 这样的永久链接。
请注意,这也非常接近 ,我收集了 64 位整数并需要将高 32 位和低 32 位分开。
请注意,Haswell 中的收集性能确实很差,但根据 Agner Fog 的说法,Skylake 在这方面做得更好(~12 周期吞吐量下降到~5)。仍然围绕着如此简单的模式进行洗牌,应该仍然比收集快很多。
我有一个按行排列的浮点数组(~20 列 x ~1M 行),我需要从中一次提取两列到两个 __m256
寄存器中。
...a0.........b0......
...a1.........b1......
// ...
...a7.........b7......
// end first __m256
一个天真的方法是
__m256i vindex = _mm256_setr_epi32(
0,
1 * stride,
2 * stride,
// ...
7 * stride);
__m256 colA = _mm256_i32gather_ps(baseAddrColA, vindex, sizeof(float));
__m256 colB = _mm256_i32gather_ps(baseAddrColB, vindex, sizeof(float));
但是,我想知道通过在一个 gather
中检索 a0, b0, a1, b1, a2, b2, a3, b3
和在另一个 a4, b4, ... a7, b7
中检索 a4, b4, ... a7, b7
是否会获得更好的性能,因为它们在内存中更接近,然后 de - 交错他们。即:
// __m256 lo = a0 b0 a1 b1 a2 b2 a3 b3 // load proximal elements
// __m256 hi = a4 b4 a5 b5 a6 b6 a7 b7
// __m256 colA = a0 a1 a2 a3 a4 a5 a6 a7 // goal
// __m256 colB = b0 b1 b2 b3 b4 b5 b6 b7
我不知道如何很好地交错 lo
和 hi
。我基本上需要 _mm256_unpacklo_ps
的对立面。我想出的最好的是:
__m256i idxA = _mm256_setr_epi32(0, 2, 4, 6, 1, 3, 5, 7);
__m256i idxB = _mm256_setr_epi32(1, 3, 5, 7, 0, 2, 4, 6);
__m256 permLA = _mm256_permutevar8x32_ps(lo, idxA); // a0 a1 a2 a3 b0 b1 b2 b3
__m256 permHB = _mm256_permutevar8x32_ps(hi, idxB); // b4 b5 b6 b7 a4 a5 a6 a7
__m256 colA = _mm256_blend_ps(permLA, permHB, 0b11110000); // a0 a1 a2 a3 a4 a5 a6 a7
__m256 colB = _mm256_setr_m128(
_mm256_extractf128_ps(permLA, 1),
_mm256_castps256_ps128(permHB)); // b0 b1 b2 b3 b4 b5 b6 b7
那是 13 个周期。有没有更好的方法?
(据我所知,预取已经在尽可能地优化朴素的方法,但由于缺乏这方面的知识,我希望对第二种方法进行基准测试。如果有人已经知道这会是什么结果,请分享一下。使用上面的去隔行扫描方法,它比原始方法慢了大约 8%。)
Edit 即使没有去隔行扫描,"proximal" gather 方法也比原始的、恒定步长的 gather 方法慢大约 6%。我认为这意味着这种访问模式混淆了硬件预取太多而不值得优化。
为了加载 32 位浮点类型的列,您可以使用内部函数 _mm256_setr_pd 和 _mm256_shuffle_ps(需要 10 个周期):
#include <iostream>
#include <immintrin.h>
inline void Print(const __m256 & v)
{
float b[8];
_mm256_storeu_ps(b, v);
for (int i = 0; i < 8; i++)
std::cout << b[i] << " ";
std::cout << std::endl;
}
int main()
{
const size_t stride = 100;
float m[stride * 8];
for (size_t i = 0; i < stride*8; ++i)
m[i] = (float)i;
const size_t stride2 = stride / 2;
double * p = (double*)m;
__m256 ab0145 = _mm256_castpd_ps(_mm256_setr_pd(p[0 * stride2], p[1 * stride2], p[4 * stride2], p[5 * stride2]));
__m256 ab2367 = _mm256_castpd_ps(_mm256_setr_pd(p[2 * stride2], p[3 * stride2], p[6 * stride2], p[7 * stride2]));
__m256 a = _mm256_shuffle_ps(ab0145, ab2367, 0x88);
__m256 b = _mm256_shuffle_ps(ab0145, ab2367, 0xDD);
Print(a);
Print(b);
return 0;
}
输出:
0 100 200 300 400 500 600 700
1 101 201 301 401 501 601 701
关于内部 _mm256_i32gather_ps 的性能,我建议查看 here。
我假设 a 和 b 被放置在 0,10,然后是 1,11 到 9,19,如果没有根据需要更改 vindexm[]
;
如果要使用聚集指令:
//#includes
#define Distance 20 // number of columns.
float a[32][20]__attribute__(( aligned(32)))= {{1.01,1.02,1.03,1.04,1.05,1.06,1.07,1.08,1.09,1.10,1.11,1.12,1.13,1.14,1.15,1.16},
{2.01,2.02,2.03,2.04,2.05,2.06,2.07,2.08,2.09,2.10,2.11,2.12,2.13,2.14,2.15,2.16},
{3.01,3.02,3.03,3.04,3.05,3.06,3.07,3.08,3.09,3.10,3.11,3.12,3.13,3.14,3.15,3.16},
{4.01,4.02,4.03,4.04,4.05,4.06,4.07,4.08,4.09,4.10,4.11,4.12,4.13,4.14,4.15,4.16},
{5.01,5.02,5.03,5.04,5.05,5.06,5.07,5.08,5.09,5.10,5.11,5.12,5.13,5.14,5.15,5.16},
{6.01,6.02,6.03,6.04,6.05,6.06,6.07,6.08,6.09,6.10,6.11,6.12,6.13,6.14,6.15,6.16},
{7.01,7.02,7.03,7.04,7.05,7.06,7.07,7.08,7.09,7.10,7.11,7.12,7.13,7.14,7.15,7.16},
{8.01,8.02,8.03,8.04,8.05,8.06,8.07,8.08,8.09,8.10,8.11,8.12,8.13,8.14,8.15,8.16},
{9.01,9.02,9.03,9.04,9.05,9.06,9.07,9.08,9.09,9.10,9.11,9.12,9.13,7.14,9.15,9.16},
{10.1,10.2,10.3,10.4,10.5,10.6,10.7,10.8,10.9,10.10,10.11,10.12,10.13,10.14,10.15,10.16},
{11.1,11.2,11.3,11.4,11.5,11.6,11.7,11.8,11.9,11.10,11.11,11.12,11.13,11.14,11.15,11.16},
{12.1,12.2,12.3,12.4,12.5,12.6,12.7,12.8,12.9,12.10,12.11,12.12,12.13,12.14,12.15,12.16},
{13.1,13.2,13.3,13.4,13.5,13.6,13.7,13.8,13.9,13.10,13.11,13.12,13.13,13.14,13.15,13.16},
{14.1,14.2,14.3,14.4,14.5,14.6,14.7,14.8,14.9,14.10,14.11,14.12,14.13,14.14,14.15,14.16},
{15.1,15.2,15.3,15.4,15.5,15.6,15.7,15.8,15.9,15.10,15.11,15.12,15.13,15.14,15.15,15.16},
{16.1,16.2,16.3,16.4,16.5,16.6,16.7,16.8,16.9,16.10,16.11,16.12,16.13,16.14,16.15,16.16}};
float tempps[8];
void printVecps(__m256 vec)
{
_mm256_store_ps(&tempps[0], vec);
printf(", [0]=%3.2f, [1]=%3.2f, [2]=%3.2f, [3]=%3.2f, [4]=%3.2f, [5]=%3.2f, [6]=%3.2f, [7]=%3.2f \n",
tempps[0],tempps[1],tempps[2],tempps[3],tempps[4],tempps[5],tempps[6],tempps[7]) ;
}
int main() {
__m256 vec1;
int vindexm [8]={0, Distance/2, Distance, Distance + Distance/2, Distance*2, Distance*2 +Distance/2, Distance*3, Distance*3 + Distance/2};
__m256i vindex = _mm256_load_si256((__m256i *) &vindexm[0]);
//loops
vec1 = _mm256_i32gather_ps (&a[0][0],vindex, 4);//place it in your loop as you want
printVecps(vec1);
return 0;
}
输出是
[0]=1.01, [1]=1.11, [2]=2.01, [3]=2.11, [4]=3.01, [5]=3.11, [6]=4.01, [7]=4.11
// __m256 lo = a0 b0 a1 b1 a2 b2 a3 b3 // load proximal elements
// __m256 hi = a4 b4 a5 b5 a6 b6 a7 b7
// __m256 colA = a0 a1 a2 a3 a4 a5 a6 a7 // goal
// __m256 colB = b0 b1 b2 b3 b4 b5 b6 b7
看来我们可以比我原来的答案更快地进行这种洗牌:
void unpack_cols(__m256i lo, __m256i hi, __m256i& colA, __m256i& colB) {
const __m256i mask = _mm256_setr_epi32(0, 2, 4, 6, 1, 3, 5, 7);
// group cols crossing lanes:
// a0 a1 a2 a3 b0 b1 b2 b3
// a4 a5 a6 a7 b4 b5 b6 b7
auto lo_grouped = _mm256_permutevar8x32_epi32(lo, mask);
auto hi_grouped = _mm256_permutevar8x32_epi32(hi, mask);
// swap lanes:
// a0 a1 a2 a3 a4 a5 a6 a7
// b0 b1 b2 b3 b4 b5 b6 b7
colA = _mm256_permute2x128_si256(lo_grouped, hi_grouped, 0 | (2 << 4));
colB = _mm256_permute2x128_si256(lo_grouped, hi_grouped, 1 | (3 << 4));
}
虽然两条指令在 Haswell 上都有 3 个周期的延迟(参见 Agner Fog) they have a single cycle throughput. This means it has a throughput of 4 cycles and 8 cycles latency. If you have a spare register which can keep the mask this should be better. Doing only two of these in parallel allows you to completly hide its latency. See godbolt and rextester。
旧答案,留作参考:
执行此随机播放的最快方法如下:
void unpack_cols(__m256i lo, __m256i hi, __m256i& colA, __m256i& colB) {
// group cols within lanes:
// a0 a1 b0 b1 a2 a3 b2 b3
// a4 a5 b4 b5 a6 a7 b6 b7
auto lo_shuffled = _mm256_shuffle_epi32(lo, _MM_SHUFFLE(3, 1, 2, 0));
auto hi_shuffled = _mm256_shuffle_epi32(hi, _MM_SHUFFLE(3, 1, 2, 0));
// unpack lo + hi a 64 bit
// a0 a1 a4 a5 a2 a3 a6 a7
// b0 b1 b4 b5 b2 b3 b6 b7
auto colA_shuffled = _mm256_unpacklo_epi64(lo_shuffled, hi_shuffled);
auto colB_shuffled = _mm256_unpackhi_epi64(lo_shuffled, hi_shuffled);
// swap crossing lanes:
// a0 a1 a2 a3 a4 a5 a6 a7
// b0 b1 b2 b3 b4 b5 b6 b7
colA = _mm256_permute4x64_epi64(colA_shuffled, _MM_SHUFFLE(3, 1, 2, 0));
colB = _mm256_permute4x64_epi64(colB_shuffled, _MM_SHUFFLE(3, 1, 2, 0));
}
从 Haswell 开始,它的吞吐量为 6 个周期(遗憾的是端口 5 上有 6 个指令)。根据 Agner Fog _mm256_permute4x64_epi64
有 3 个周期的延迟。这意味着 unpack_cols
的延迟为 11 8 个周期。
您可以查看 godbolt.org or test it at rextester 上的代码,它支持 AVX2,但遗憾的是没有像 godbolt 这样的永久链接。
请注意,这也非常接近
请注意,Haswell 中的收集性能确实很差,但根据 Agner Fog 的说法,Skylake 在这方面做得更好(~12 周期吞吐量下降到~5)。仍然围绕着如此简单的模式进行洗牌,应该仍然比收集快很多。