3

我试图尝试long ints使用AVX2. 我是SIMD编程的新手,我不知道从哪里开始。我没有看到任何解释如何执行minmaxin的帖子/示例AVX2long ints我知道由于限制,我不能超过 4 256 bit,但我可以使用三个步骤来解决我的问题。我也无法弄清楚如何将已经存在的正常数据加载long int arrayvectorsfor 中avx2

我知道这个过程背后的想法,这就是我想要实现的目标

long int nums = {1 , 2, 3 , 4 , 5 , 6 , 7, 8}
a = min(1,2) ; b = min(3,4) ; c = min(5,6) ; d = min(7,8)
x = min(a,b) ; y = min(c,d)
answer  = min(x,y)

有人可以帮我解决如何让它发挥作用。还有最后min是单人操作,是不是就做比较好CPU?我应该使用其他东西AVX2吗?(我在x86系统上)

4

1 回答 1

5

对于 x86 优化等,请参阅https://stackoverflow.com/tags/x86/info上的链接。特别是。英特尔的内在指南和 Agner Fog 的东西。

如果你总是正好有 8 个元素(64 个字节),那会大大简化事情。向量化小东西时的主要挑战之一是不增加太多的启动/清理开销来处理不填充整个向量的剩余元素。

AVX2 没有压缩 64 位整数的最小/最大指令。只有 8、16 和 32。这意味着您需要使用生成掩码的比较来模拟它(条件为假的元素全为 0,条件为真的元素全为 1,因此您可以使用此掩码将元素归零在其他向量中。)为了节省实际执行 AND/ANDN 和 OR 操作以将事物与掩码组合,有混合指令。

AVX-512将为这个操作带来很大的加速。(支持进来(仅限至强)Skylake)。它有一个_mm_min_epi64. 此操作还有一个库函数:__int64 _mm512_reduce_min_epi64 (__m512i a). 我假设这个内在函数会发出一系列vpminsq指令。英特尔在其内部查找器中列出了它,但它只是一个英特尔库函数,而不是机器指令。

这是一个应该可以工作的 AVX2 实现。我还没有测试过,但编译后的输出看起来像是正确的指令序列。我可能在某处得到了相反的比较,所以检查一下。

操作原理是:得到两个256b向量的elementwise min。将其拆分为两个 128b 向量并获得其元素最小值。然后将两个 64b 值的向量带回 GP 寄存器并执行最后的最小值。最大值同时完成,与最小值交错。

(糟糕,您在问题中提到了最小值/最大值,但现在我看到您实际上只想要最小值。删除不需要的部分是微不足道的,您可以将其更改为返回值,而不是通过指针/引用存储结果。标量版本可能更快;在您的应用程序使用此操作的上下文中进行更好的测试(不是独立的微基准测试)。)

#include <stdint.h>
#include <immintrin.h>

int64_t input[8] = { 1, 2, 3, };

#define min(a,b) \
   ({ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); \
     _a < _b ? _a : _b; })

#define max(a,b) \
   ({ __typeof__ (a) _a = (a); \
       __typeof__ (b) _b = (b); \
     _a > _b ? _a : _b; })

// put this where it can get inlined.  You don't want to actually store the results to RAM
// or have the compiler-generated VZEROUPPER at the end for every use.
void minmax64(int64_t input[8], int64_t *minret, int64_t *maxret)
{
    __m256i *in_vec = (__m256i*)input;
    __m256i v0 = in_vec[0], v1=in_vec[1];  // _mm256_loadu_si256 is optional for AVX

    __m256i gt = _mm256_cmpgt_epi64(v0, v1); // 0xff.. for elements where v0 > v1.  0 elsewhere
    __m256i minv = _mm256_blendv_epi8(v0, v1, gt);  // take bytes from v1 where gt=0xff (i.e. where v0>v1)
    __m256i maxv = _mm256_blendv_epi8(v1, v0, gt);  // input order reversed

    /* for 8, 16, or 32b:  cmp/blend isn't needed
       minv = _mm256_min_epi32(v0,v1);
       maxv = _mm256_min_epi32(v0,v1);  // one insn shorter, but much faster (esp. latency)
       And at the stage of having a 128b vectors holding the min and max candidates,
       you'd shuffle and repeat to get the low 64, and optionally again for the low 32,
       before extracting to GP regs to finish the comparisons.
     */

    __m128i min0 = _mm256_castsi256_si128(minv); // stupid gcc 4.9.2 compiles this to a vmovdqa
    __m128i min1 = _mm256_extracti128_si256(minv, 1);  // extracti128(x, 0) should optimize away to nothing.

    __m128i max0 = _mm256_castsi256_si128(maxv);
    __m128i max1 = _mm256_extracti128_si256(maxv, 1);

    __m128i gtmin = _mm_cmpgt_epi64(min0, min1);
    __m128i gtmax = _mm_cmpgt_epi64(max0, max1);
    min0 = _mm_blendv_epi8(min0, min1, gtmin);
    max0 = _mm_blendv_epi8(max1, max0, gtmax);

    int64_t tmp0 = _mm_cvtsi128_si64(min0);    // tmp0 = max0.m128i_i64[0];  // MSVC only
    int64_t tmp1 = _mm_extract_epi64(min0, 1);
    *minret = min(tmp0, tmp1);  // compiles to a quick cmp / cmovg of 64bit GP registers

    tmp0 = _mm_cvtsi128_si64(max0);
    tmp1 = _mm_extract_epi64(max0, 1);
    *maxret = min(tmp0, tmp1);
}

这可能会也可能不会比在 GP 寄存器中执行整个操作更快,因为 64 位加载是 1 uop,cmp是 1 uop,并且cmovcc只有 2 uop(在 Intel 上)。Haswell 每个周期可以发出 4 个微指令。在你到达比较树的底部之前,还有很多独立的工作要做,即便如此,cmp 是 1 个周期延迟,而 cmov 是 2。如果你同时交错工作一个最小值和一个最大值时间,有两个独立的依赖链(在这种情况下是树)。

矢量版本的延迟比吞吐量要高得多。如果您需要对多个独立的 8 个值集进行此操作,则矢量版本可能会做得很好。否则, 的 5 个周期延迟pcmpgt*和 的 2 个周期延迟blendv会受到影响。如果有其他独立的工作可以并行发生,那很好。

如果您有较小的整数,pmin*(有符号或无符号,8、16 或 32b)是 1 个周期延迟,每周期 2 个吞吐量。仅对于 16b 无符号元素,甚至还有一个水平 min 指令,它可以在一个向量中为您提供 8 个中的 min 元素,正如 user-number-guy 评论的那样。这消除了将最小候选者缩小到适合一个向量后所需的整个拆分/最小缩小过程。

于 2015-07-25T14:50:49.337 回答