我有一个按行排列的 float 组(~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
在另一个中,因为它们在内存中更接近,然后将它们解交错。即:
// __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 个周期。有没有更好的办法?
(据我所知,prefetch 已经尽可能地优化了朴素的方法,但由于缺乏这方面的知识,我希望对第二种方法进行基准测试。如果有人已经知道这会是什么结果,请分享. 使用上面的去隔行方法,它比朴素的方法慢了大约 8%。)
编辑 即使没有去隔行,“近端”聚集方法也比简单的恒定步幅聚集方法慢 6% 左右。我认为这意味着这种访问模式混淆了硬件预取太多,以至于不值得优化。
最佳答案
// __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),但它们只有一个周期的吞吐量。这意味着它具有4 个周期 的吞吐量和8 个周期 的延迟。如果您有一个可以保留掩码的备用寄存器,那应该会更好。仅并行执行其中两个操作可以让您完全隐藏其延迟。参见 godbolt和 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 上的代码或在 rextester 进行测试它支持 AVX2,但遗憾的是没有像 godbolt 这样的永久链接。
请注意,这也非常接近 the problem I had我在那里收集了 64 位整数,需要将高 32 位和低 32 位分开。
请注意,Haswell 中的收集性能确实很差,但根据 Agner Fog 的说法,Skylake 在这方面做得更好(~12 周期吞吐量下降到~5)。仍然围绕这种简单的模式洗牌应该仍然比收集快得多。
关于c++ - 打包和解交错两个 __m256 寄存器,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42497985/