C++ AVX2中冲突检测的回退实现

C++ AVX2中冲突检测的回退实现,c++,x86,intrinsics,avx2,avx512,C++,X86,Intrinsics,Avx2,Avx512,AVX512CD包含内在的\u mm512\u conflict\u epi32(\uuuu m512i a)它返回一个向量,如果a中的每个元素具有相同的值,则设置一个位。在AVX2中有没有类似的方法 我对extact位不感兴趣,我只需要知道哪些元素是左边(或右边)元素的副本。我只是想知道分散是否会产生冲突 基本上,我需要一个AVX2等效的 __mm256i detect_conflict(__mm256i a) { __mm256i cd = _mm256_conflict_epi32(a

AVX512CD包含内在的
\u mm512\u conflict\u epi32(\uuuu m512i a)
它返回一个向量,如果
a
中的每个元素具有相同的值,则设置一个位。在AVX2中有没有类似的方法

我对extact位不感兴趣,我只需要知道哪些元素是左边(或右边)元素的副本。我只是想知道分散是否会产生冲突

基本上,我需要一个AVX2等效的

__mm256i detect_conflict(__mm256i a) {
  __mm256i cd = _mm256_conflict_epi32(a);
  return _mm256_cmpgt_epi32(cd, _mm256_set1_epi32(0));
}

我能想到的唯一方法是使用
\u mm256\u permutevar8x32\u epi32()
将每个值右移1(穿过通道),然后进行七次比较,屏蔽未编码的位,然后将它们放在一起非常慢。

TL:DR:由于完全检测哪些元素冲突的成本很高,因此可能值得做更多的回退工作,以换取更便宜的检测。这取决于您的冲突处理选项/策略

我想出了一个相当有效的方法来检查是否存在冲突,而不查找冲突的位置,如。它实际上比你的速度快,但当然它给你的信息要少得多。(KNL具有fast
vpconflictd

如果存在任何冲突,可以对所有元素使用完全标量回退。如果冲突非常罕见,分支预测失误不会影响性能,那么这将非常有效。(不过,AVX2一开始没有分散指令,所以我不确定您到底需要它做什么。)

唯一左边或唯一右边的行为是困难的,但我的方法可以为您提供一个元素与任何其他元素冲突的掩码(例如
v[0]==v[3]
将导致
conflict[0]
conflict[3]
均为真)。这只需要额外的1次洗牌,或者考虑到这个目标重新设计可能需要0次洗牌

(一开始我误解了这个问题;我以为你想检查两个方向,而不是谈论两种不同的实现选项,以了解
vpconflictd
所做的大部分工作。实际上,一开始我以为你只是想检查是否有人在场,比如
bool any_conflicts(u m256i)


查找是否存在任何冲突:
bool any_conflicts 32(\uu m256i)

共有28个标量比较。这是3.5个压缩比较向量。我们的目标应该是使用4个向量比较,这为一些冗余留出了空间

为这些比较创建输入将需要洗牌,其中一些必须是车道交叉。4唯一比较至少需要4个向量(包括初始未缓冲副本),因为3选择2仅为3

理想情况下,尽可能少的洗牌是车道交叉,并且有很多用于比较和比较结果的ORing。如果洗牌不需要向量洗牌控件,只需要一个
imm8
,也很好。如果他们在AMD Ryzen上速度不慢的话也很好,因为AMD Ryzen将256b指令解码为多个128b UOP。(在这方面,有些洗牌比其他洗牌更糟糕,例如,
vperm2i128
非常糟糕;在交换单个向量的高半部和低半部时,比
vpermq
糟糕得多。不幸的是,即使使用
-mtune=znver1
,clang也会出错,并在需要时将
\u mm256\u permute4x64\u epi64
编译成
vperm2i128
)可以)

我很早就找到了一个解决方案,可以实现这些目标中的大部分:3次洗牌,4次比较。其中一个洗牌在跑道上。它们都使用立即控制字节而不是向量

// returns a 0 or non-zero truth value
int any_conflicts32(__m256i v)
{
    __m256i hilo       = _mm256_permute4x64_epi64(v, _MM_SHUFFLE(1,0,3,2));  // vpermq is much more efficient than vperm2i128 on Ryzen and KNL, same on HSW/SKL.
    __m256i inlane_rotr1 = _mm256_shuffle_epi32(v, _MM_SHUFFLE(0,3,2,1));
    __m256i full_rotl2 = _mm256_permute4x64_epi64(v, _MM_SHUFFLE(2,1,0,3));

    __m256i v_ir1 = _mm256_cmpeq_epi32(v, inlane_rotr1);
    __m256i v_hilo= _mm256_cmpeq_epi32(v, hilo);           // only really needs to be a 128b operation on the low lane, with leaving the upper lane zero.
                                                           // But there's no ideal way to express that with intrinsics, since _mm256_castsi128_si256 technically leaves the high lane undefined
                                                           // It's extremely likely that casting down and back up would always compile to correct code, though (using the result in a zero-extended register).
    __m256i hilo_ir1 = _mm256_cmpeq_epi32(hilo, inlane_rotr1);
    __m256i v_fl2 = _mm256_cmpeq_epi32(v, full_rotl2);

    __m256i t1 = _mm256_or_si256(v_ir1, v_hilo);
    __m256i t2 = _mm256_or_si256(t1, v_fl2);
    __m256i conflicts = _mm256_or_si256(t2, hilo_ir1);    // A serial dep chain instead of a tree is probably good because of resource conflicts from limited shuffle throughput

    // if you're going to branch on this, movemask/test/jcc is more efficient than ptest/jcc

    unsigned conflict_bitmap = _mm256_movemask_epi8(conflicts);  // With these shuffles, positions in the bitmap aren't actually meaningful
    return (bool)conflict_bitmap;
    return conflict_bitmap;
}
我是如何设计这个的

我制作了一个包含所有需要检查的元素对的表,并制作了列,洗牌操作数可以满足这一要求

我开始时做了一些洗牌,这些洗牌可以便宜地完成,结果证明我早期的猜测很有效

我的设计笔记:

    // 7 6 5 4 | 3 2 1 0

    // h g f e | d c b a
    // e h g f | a d c b    // inlanerotr1 = vpshufd(v)
    // f e d c | b a h g    // fullrotl2 = vpermq(v)

    // d c b a | h g f e    // hilo = vperm2i128(v) or vpermq.  v:hilo has lots of redundancy.  The low half has all the information.

          v:lrot1      v:frotr2     lrotr1:frotl2                (incomplete)
 * ab   [0]v:lrotr1                 [3]lr1:fl2
 * ac                  [2]v:frotl2
 * ad   [3]v:lrotr1                 [2]lr1:fl2
 * ae                                                                           [0,4]v:hilo
 * af                                           [4]hilo:lrotr1
 * ag                  [0]v:frotl2
 * ah                                           [3]hilo:lrotr1

 * bc   [1]v:lrotr1
 * bd                  [3]v:frotl2                               [5]hilo:frotl2
 * be                                           [0]hilo:lrotr1
 * bf                                                                           [1,5]v:hilo
 * bg                               [0]lr1:fl2  [5]hilo:lrotr1
 * bh                  [1]v:frotl2

 * cd   [2]v:lrotr1
 * ce                  [4]v:frotl2  [4]lr1:fl2
 * cf                                           [1]hilo:lrotr1
 * cg                                                                           [2,6]v:hilo
 * ch                               [1]lr1:fl2  [6]hilo:lrotr1

 * de                                           [7]hilo:lrotr1
 * df                  [5]v:frotl2                               [7]hilo:frotl2
 * dg                               [5]lr1:fl2  [2]hilo:lrotr1
 * dh                                                                           [3,7]v:hilo

 * ef   [4]v:lrotr1                 [7]lr1:fl2
 * eg                  [6]v:frotl2
 * eh   [7]v:lrotr1                 [6]lr1:fl2

 * fg   [5]v:lrotr1
 * fh                  [7]v:frotl2

 * gh   [6]v:lrotr1

 */
事实证明,在通道rotr1==full rotl2中有大量冗余,因此不值得使用。结果还表明,在
v==hilo
中拥有所有允许的冗余也可以

如果您关心哪个结果在哪个元素中(而不仅仅是检查是否存在), 然后
v==swap\u hilo(lrotr1)
可以代替
lrotr1==hilo
。 但是我们也需要交换hilo(v),所以这意味着额外的洗牌

我们可以在hilo==lrotr1之后洗牌,以获得更好的ILP。 或者可能有一套不同的洗牌,让我们拥有一切。 也许如果我们考虑VPRMD的矢量洗牌控制…< /P>
编译器asm输出与最佳asm :

哈斯韦尔有一个洗牌单位(在5号门上)

因此,最佳情况下的延迟是8个周期来准备一个向量,给定此序列中其他指令的资源冲突,但假设与仍在管道中的过去指令没有冲突。(应该是7个周期,但是gcc重新排列了我的内在函数的依赖结构,使更多的东西依赖于上一次洗牌结果的比较。)

这比具有17c延迟的每10c吞吐率快一个。(当然,这给了你更多的信息,@harold对它的模拟需要更多的说明)

幸运的是,gcc没有重新安排洗牌并引入潜在的回写冲突。(例如,将
vpshufd
放在最后将意味着以最旧的第一顺序将随机UOP分派到端口5将使
vpshufd
在与第一个
vpermq
相同的周期内准备就绪(1c延迟vs.3c)。)gcc对一个版本的代码执行了此操作(其中我比较了错误的变量),因此,gcc
-mtune=haswell
似乎没有考虑到这一点。(也许这没什么大不了的,我还没有测量过对延迟的真正影响。我知道调度器在从预订站挑选UOP方面很聪明
   # assume ymm0 ready on cycle 0
    vpermq  ymm2, ymm0, 78     # hilo ready on cycle 3 (execution started on cycle 0)
    vpshufd ymm3, ymm0, 57     # lrotr1 ready on cycle 2  (started on cycle 1)
    vpermq  ymm1, ymm0, 147    # frotl2 ready on cycle 5  (started on 2)
    vpcmpeqd  ymm4, ymm2, ymm0  # starts on 3, ready on 4
    vpcmpeqd  ymm1, ymm1, ymm0  # starts on 5, ready on 6
    vpcmpeqd  ymm2, ymm2, ymm3  # starts on 3, ready on 4
    vpcmpeqd  ymm0, ymm0, ymm3  # starts on 2, ready on 3
    vpor    ymm1, ymm1, ymm4    # starts on 6, ready on 7
    vpor    ymm0, ymm0, ymm2    # starts on 4, ready on 5
    vpor    ymm0, ymm1, ymm0    # starts on 7, ready on 8
         # a different ordering of VPOR merging could have saved a cycle here.  /scold gcc
    vpmovmskb       eax, ymm0
    vzeroupper
    ret