我有一个按行排列的浮点数(〜20 cols 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));

但是,我想知道是否可以通过在一个a0, b0, a1, b1, a2, b2, a3, b3中检索gather和在另一个a4, b4, ... a7, b7中检索lo来获得更好的性能,因为它们在内存中更近,然后对其进行解交织。那是:
// __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

我不知道如何很好地交错hi_mm256_unpacklo_ps。我基本上需要ojit_code的反面。我想出的最好的方法是:
__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%。)

编辑即使没有去隔行扫描,“近端”聚集方法也要比幼稚的,恒定步幅的聚集方法慢大约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个周期的延迟。如果您有一个备用寄存器可以保留掩码,那应该更好。并行执行其中的两个操作可以完全隐藏其延迟。参见godboltrextester

旧答案,仅供引用:

进行这种改组的最快方法如下:
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上检查代码,也可以在具有AVX2支持的rextester上对其进行测试,但遗憾的是,没有像Godbolt这样的永久链接。

请注意,这也非常接近the problem I had,在这里我收集了64位整数,并且需要将高32位和低32位分开。

请注意,在Haswell中收集性能确实很差,但是根据Agner Fog Skylake的说法,它的性能要好得多(将12个循环的吞吐量降低到5个左右)。仍然围绕着这种简单的模式进行改组应该比收集起来要快得多。

08-16 10:25