【问题标题】:Packing and de-interleaving two __m256 registers打包和解交织两个 __m256 寄存器
【发布时间】:2017-07-18 18:55:31
【问题描述】:

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

但是,我想知道是否可以通过在一个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

我不知道如何很好地交错lohi。我基本上需要_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%。我认为这意味着这种访问模式过于混淆了硬件预取,因此不值得进行优化。

【问题讨论】:

  • a0和b0相差多少?
  • @FackedDeveloper 在运行时在 0 到 19 之间变化(即任何一对列)。
  • FWIW,x86 上的当前收集实现不利用连续元素,即,没有来自相邻地址的负载合并 - 每个负载单独发送到负载端口,所以你可以' t 目前打破每个循环 2 次负载的障碍(即 8 次负载需要 4 次循环)。这在未来可能会改变:高性能聚集背后的想法之一是它检测元素何时相邻或重叠,并在这种情况下发出更少的负载。所以有一天你的第二个策略可能会更快。

标签: c++ x86 simd avx avx2


【解决方案1】:

为了加载 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

【讨论】:

  • 有趣的方法,谢谢。似乎最大的加速来自您正在加载元素 nn + 1 的事实,不幸的是,这并不总是我需要访问的(抱歉没有在问题中说明这一点)。不过,最好选择 0145/2367 来避免交叉车道。 -- 顺便说一句,我的 13 个循环数只是用于打包,而不是移动指令。
  • 使用答案,它会做任何你想做的事。制作一个内联函数并将其设计为在索引上执行将它们更改为此答案帮助中的一些兼容数字和说明。
【解决方案2】:

我假设 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 

【讨论】:

  • 这与上面使用 _mm256_i32gather_ps 的 OP 的原始“naïve”实现有何不同?
  • @PaulR,OP 使用两个集合来加载两列,然后将结果重新排序为两个向量。这会将重新排序的元素收集到一个向量中。这不是吗?我可能不明白这个问题。
  • 我没有仔细研究过,但乍一看它看起来一样 - 相同的偏移量等 - 但我可能错了吗?...
  • 不,你没有错。我刚研究了一下,我想我理解错了。
  • 没问题 - 无论如何,收集的负载效率很低(至少在当前的 CPU 上) - 我猜 OP 确实需要一个使用正常负载和一些洗牌/置换的高效实现。
【解决方案3】:
// __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 上查看代码或在 rextester 上测试它,它支持 AVX2,但遗憾的是没有像 Godbolt 这样的永久链接。


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


请注意,Haswell 中的收集性能确实很差,但根据 Agner Fog Skylake 的说法,它的性能要好得多(约 12 个周期的吞吐量降至约 5 个)。仍然在如此简单的模式中进行改组应该仍然比收集要快得多。

【讨论】:

  • shuffle_epi32 + 2 blend_epi32 + 2 permutate4x64 应该有 3 个周期的吞吐量/6 个周期的延迟。我得回家检查一下。
  • 注意:我们可以替换colA = _mm256_permute2x128_si256(lo_grouped, hi_grouped, 0 | (2 &lt;&lt; 4));。使用简单的插入 colA = _mm256_inserti128_si256(lo_grouped, _mm256_castsi256_si128(hi_grouped), 1); 但遗憾的是两者具有相同的延迟。
  • Zen1 的效率要高得多 vinsert128 所以这实际上是一个好主意,因为它在任何地方都不会更糟。 Zen2 与 Intel 和 vperm2i128 相同,尽管它比 Intel 慢于 vpermd / vpermq (_mm256_permute4x64_epi64) - 2 微秒,6c 延迟。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-03-08
  • 2011-01-14
相关资源
最近更新 更多