使用 AVX2 计算 8 个长整数的最小值

Posted

技术标签:

【中文标题】使用 AVX2 计算 8 个长整数的最小值【英文标题】:Calculating min of 8 long ints using AVX2 【发布时间】:2015-07-25 05:45:03 【问题描述】:

我试图使用AVX2 来查找 8 long ints 的最小值。我是SIMD 编程的新手,我不知道从哪里开始。我没有看到任何解释如何在AVX2 中执行minmax 的帖子/示例。我知道由于256 bit 的限制,我不能超过4 个long ints,但我可以使用三个步骤来解决我的问题。我也无法弄清楚如何将已经存在的普通long int array 的数据加载到vectors 中以获取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系统上)

【问题讨论】:

如果你的数字适合无符号 16 位,你可以使用 PHMINPOSUW 指令,它代表 Packed Horizo​​ntal MINUnsigned Words 的最大和 POSition。在Intel Intrinsics Guide 中,我没有找到相应的内在函数,here 来自 Microsoft。有一个 VEX 版本可以清除目标寄存器的高 128 位以避免错误依赖。也许更专业的人可以告诉你更好的方法。 8 个整数不方便 AVX(太少)。如果您添加有关更高级别代码的更多信息,我们可以为您提供更好的帮助 =) 数据正好是 8 个长整数,它们总是很大的数字。 @stgatilov 它只是一个简单的函数来查找将不断调用的最小值。如果AVX不好,我可以切换到其他语言。 【参考方案1】:

有关 x86 优化等,请参阅https://***.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 寄存器并执行最后的最小值。最大值同时完成,与最小值交错。

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

#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。如果你同时交错工作一个 min 和一个 max时间,有两个独立的依赖链(在这种情况下是树)。

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

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

【讨论】:

有一件事我无法理解。由于提取下半部分无需任何操作即可完成,编译器不应该自动将提取内在函数替换为强制转换吗?参数是立即的,即编译时常量。 哦,看起来 gcc 4.9.2 确实将其编译为与强制转换相同。 (并且 arg 始终必须是编译时常量,因为它必须在指令中放入 imm8。)IDK 如果编译器一直都很聪明,或者如果存在可能生成更糟糕代码的风险其他(尤其是较旧的)编译器。我测试了extracti128(仍然是无用的vmovdqa,而不是引用旧值)和extract_epi64vmovq)。 clang 3.5 也是如此(优化了extract(..., 0)),并且没有gcc 为extracti128(0) 发出无用的vmovdqa %xmm4,%xmm3 的问题。即使在-O0。为我不想发出的指令编写内在函数仍然感觉很奇怪。在 asm 中,你可以写 vpextrq $0, %xmm0, %rcx,它会像 vmovq 一样工作,但速度较慢。 对我来说,使用完全不同的函数来访问该对的第 0 部分和第 1 部分是很奇怪的。我想这可能会导致模板代码出现问题,当您不想编写 if-s 来检查索引是否为零时。 是的,这是一个很好的论点。在您提出之前,我只是假设内在函数直接映射到 asm 指令(加载/存储除外)。英特尔的内在函数指南并没有说 extract_epi64 有时可以是 movqpextrq 是列出的唯一指令。显然,这是一个很好的优化,可以生成更好的代码,并让您在编写代码时更加一致。

以上是关于使用 AVX2 计算 8 个长整数的最小值的主要内容,如果未能解决你的问题,请参考以下文章

如何找到不在整数字段中的最小值

2022年雪花算法的最大与最小值

c语言:输入两个整数,计算并输出这两个整数的和·平均数·最大值·最小值?

c语言:输入两个整数,计算并输出这两个整数的和·平均数·最大值·最小值?

c语言计算数组的长度,最大最小值 补全程序?

算法训练 最大值与最小值的计算