C++ AVX2中冲突检测的回退实现
AVX512CD包含内在的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
\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具有fastvpconflictd
)
如果存在任何冲突,可以对所有元素使用完全标量回退。如果冲突非常罕见,分支预测失误不会影响性能,那么这将非常有效。(不过,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