我有以下问题需要使用除AVX2以外的任何其他方法来解决。
我有3个值存储在m128i变量中(不需要第4个值),需要将这些值移动4,3,5。我需要两个功能。一个用于右侧逻辑移位,另一个用于左侧逻辑移位。
有没有人知道使用SSE / AVX解决问题的方法?我唯一能找到的是_mm_srlv_epi32()
AVX2。
添加更多信息。这是我尝试使用SSE / AVX进行优化的代码。这是我的草稿/跳棋 - 发动机的一部分。
uint32_t Board::getMoversBlack(){
const uint32_t nocc=~(BP|WP);
const uint32_t BK = BP & K;
uint32_t movers = (nocc >> 4) & BP;
movers |= ((nocc & MASK_R3) >>3) & BP;
movers |= ((nocc & MASK_R5) >>5) & BP;
if (BK != 0) {
movers |= (nocc << 4) & BK;
movers |= ((nocc & MASK_L3) << 3) & BK;
movers |= ((nocc & MASK_L5) <<5) & BK;
}
return movers;
}
感谢任何帮助。
答案 0 :(得分:1)
如果您真的需要这个(并且无法通过重新排列数据来避免它),您可以完全/安全地模拟_mm_srlv_epi32
,而不会破坏任何高位或低位。
对于编译时常量计数,您可以使用左右移位混合使用其中大部分。
可能选择不好:
打开标量:yuck。对于编译时常量计数有点不好,但对于运行时变量计数更糟糕,特别是如果您必须解包计数向量。没有BMI2 shrx
的x86变量计数移位具有笨重的语义,并在Intel SnB系列上解码为多个uop。他们还会采取额外的mov
指示,以便将班次计入cl
,如果它不在那里。
进行单独的移位然后混合以从移动了该数量的向量中获取元素。这并不是很好,但您可以通过在复制它们时将不需要的元素归零来降低混合成本。 (例如,如果已知高元素为零,则使用pshufd
进行复制,以从{0,22,0,0}
的起始向量获取{11,22,33, 0}
的向量,并重复{0,0,33,0}
。)
所以,将你不使用的高元素归零,将2x pshufd复制+ shuffle零到位,3x psrld用不同的计数,以及你没有复制的向量中的其他元素,然后将3个向量重新组合在一起。 (如果你不使用矢量的一个元素,这需要更多的工作。)
根据代码的其余部分和微体系结构,使用shuffle而不是MOVDQA + PAND可能不值得。如果任何元素使用相同的移位计数,则此选项变得更具吸引力。
此外,您可以将低元素与movss
混合到一个向量中,并将低半部分与movsd
混合。那些使用shuffle端口,因此shuffle吞吐量可能是一个问题。这实际上可能非常稳固。
希望有更好的选择。
Marc建议的SSE2版本(见下文)也适用于完全一般的情况。
当最小和最大班次计数之差<=最小班次计数时,您可以use @Marc's SSE4.1 suggestion 使用乘法作为变量左移来计算右移计数的差异。或者单独作为左移。对于大多数情况来说,这可能是最好的,即使vector-int乘法很慢,也会减少很少的指令。
__m128i srlv435_sse4(__m128i v)
{
__m128i rshift = _mm_srli_epi32(v, 3); // v >> 3
// differences in shift count by multiplying by powers of 2
__m128i vshift = _mm_mullo_epi32(rshift, _mm_setr_epi32(2,4,1,0)); // [ x >> 2, y >> 1, z >> 3, 0 ] Except with low bits truncated.
__m128i shift2 = _mm_srli_epi32(vshift, 2); // [ x >> 4, y >> 3, z >> 5, 0 ]
return shift2;
}
这很好,因为即使没有AVX1,它也可以在没有编译器需要任何MOVDQA指令来复制寄存器的情况下就地运行。
请注意, SSE4.1 _mm_mullo_epi32
速度不快:Haswell上的p0为2 uops:10c延迟,每2c吞吐量为1。 Skylake的吞吐量更高,其中2个uop中的每一个都可以在p0或p1上运行,但仍然依赖于10c延迟。 (http://agner.org/optimize/标记维基中的x86和其他链接。)
这在Haswell之前具有更好的延迟,其中pmulld
是单uop指令(~5个周期,1c吞吐量),而不是10个周期的2个相关uop。
在AMD Bulldozer系列和Ryzen上,延迟= 4或5,吞吐量= pmulld每2c一个。
我没有通过向量转换来检查端口冲突。
没有SSE4.1 ,您可以使用2x SSE2 _mm_mul_epu32
一次进行2次乘法运算。排列奇数元素(1和3),pshufd
复制+将它们拖拽到位置0和2,pmuludq
寻找它们。
从偶数2个32位元素产生2个64位结果,因此您不需要预移位以避免溢出。当移位计数之间的差异大于最小移位时,它也意味着可以安全使用,因此SSE4.1方式不能保留元素中保留位最多的所有必需位。
// general case: substitute in *any* shift counts and it still works.
__m128i srlv_sse2(__m128i v) // [x y z w]
{
__m128i vs_even = _mm_mul_epu32(v, _mm_setr_epi32(1U<<1, 1U<<2, 1U<<0, 0)); // [ x<<1 z<<0 ] (64-bit elements)
// The 4 (1U<<2) is unused, but this lets us share a constant with the SSE4 version, saving rodata size. (Compilers optimize duplicate constants for you; check the disassembly for same address)
vs_even = _mm_srli_epi64(vs_even, 5); // [ x>>4 0 x>>5 0 ] (32-bit elements ready for blending with just an OR)
__m128i odd = _mm_shuffle_epi32(v, _MM_SHUFFLE(3, 3, 1, 1));
__m128i vs_odd = _mm_mul_epu32(v, _mm_setr_epi32(1U<<(32-3),0,0,0)); // [ (y<<32) >> 3 0 ] (64-bit elements)
// If any elements need left shifts, you can't get them all the way out the top of the high half with a 32-bit power of 2.
//vs_odd = _mm_slli_epi64(vs_odd, 32 - (3+2)); // [ garbage, y>>3, 0, 0 ]
// SSE2 doesn't have blend instructions, do it manually.
__m128i vs_oddhi = _mm_and_si128(vs_odd, _mm_setr_epi32(0, -1, 0, -1));
__m128i shifted = _mm_or_si128(vs_even, vs_oddhi);
return shifted;
}
这里有一些明显的优化:
你的情况不是使用第4个元素,所以第2个乘法是没有意义的:只需移动并使用AND掩码来清除高元素。 vs_odd = _mm_srli_epi32v, 3);
并使用0,-1,0,0
作为您的AND掩码。
不是左移1和0,而是将x添加到自身并保持z不变。使用高64位的归零复制向量非常便宜(movq
),但不如movdqa
便宜(在具有mov-elimination的CPU上)。
__m128i rshift = _mm_srli_epi32(v, 3); // v >> 3
__m128i xy00 = _mm_move_epi64(rshift);
__m128i vshift = _mm_add_epi32(rshift, xy00); // [ x >> 2, y >> 2, z >> 3, 0 ]
但这并不能处理y
。我们可以将y>>2
与vshift
隔离开来并再次添加以生成y>>1
。 (但请记住不要使用y>>3
中的旧xy00
。
我们还可以考虑使用_mm_mul_epu32
(pmuludq
)一次,并使用复制+ shift + AND进行另一步骤(从原始v
而不是rshift
复制到缩短dep链)。这在你的情况下非常有用,因为你没有使用top元素,因此只有一个有效的奇数元素,因此你不需要变量。
结合movq
,movss
和movsd
,这里可能会有更多的东西从基本上分别移动3个元素。在端口压力,延迟,uop计数(前端吞吐量)和诸如此类的东西之间存在权衡。例如我在想
movdqa xmm1, xmm0
psrld xmm0, 3 # [ x>>3 y>>3 garbage ]
psrld xmm1, 4 # [ x>>4 y>>4 garbage ]
movss xmm1, xmm0 # [ x>>3 y>>4 garbage ] # FP shuffle
psrld xmm0, 2 # [ garbage z>>5 ]
movsd xmm0, xmm1 # [ x>>3 y>>4 z>>5 ] # FP shuffle
例如,Haswell每个时钟吞吐量只有1个移位,所以这并不奇妙。与乘法选项相比,它具有相当好的延迟。它在Skylake上很不错,其中2个端口可以立即进行向量转换。
整数指令之间的FP shuffle在除Nehalem之外的Intel CPU上是很好的(其中每个方向都有2个周期的旁路延迟延迟惩罚,但吞吐量仍然可以)。我认为AMD也很好。
当然所有这些CPU都有SSE4.1,所以如果使用动态运行时调度,SSE2版本只需要在Core2 / K10上运行。 (我猜老Atom,或其他什么)。
答案 1 :(得分:0)
SSE2英特尔架构指令集扩展引入了整数移位操作。下面我插入了一个可用的编译器内在函数列表,在SSE2中实现逻辑移位操作:
psllw
__m128i _mm_sll_epi16 (__m128i a, __m128i count)
pslld
__m128i _mm_sll_epi32 (__m128i a, __m128i count)
psllq
__m128i _mm_sll_epi64 (__m128i a, __m128i count)
psllw
__m128i _mm_slli_epi16 (__m128i a, int imm8)
pslld
__m128i _mm_slli_epi32 (__m128i a, int imm8)
psllq
__m128i _mm_slli_epi64 (__m128i a, int imm8)
pslldq
__m128i _mm_slli_si128 (__m128i a, int imm8)
psrlw
__m128i _mm_srl_epi16 (__m128i a, __m128i count)
psrld
__m128i _mm_srl_epi32 (__m128i a, __m128i count)
psrlq
__m128i _mm_srl_epi64 (__m128i a, __m128i count)
psrlw
__m128i _mm_srli_epi16 (__m128i a, int imm8)
psrld
__m128i _mm_srli_epi32 (__m128i a, int imm8)
psrlq
__m128i _mm_srli_epi64 (__m128i a, int imm8)
psrldq
__m128i _mm_srli_si128 (__m128i a, int imm8)
有关详情,请访问Intel Intrinsics Guide网站。
如果上述内在函数将所有值移位相同位数的限制并不适合每个人,则可以使用乘以2的幂并除以2的幂,但这会对性能产生重大影响。可能右移3个32位整数,每个不同的值将比矢量分割更快。乘法也是如此,但首先我会在代码中测试它。