通过Intel AVX使用掩码进行洗牌

9

我是AVX编程的新手。我有一个需要洗牌的寄存器。我想从256位寄存器R1中洗牌几个字节到一个空寄存器R2中。我想定义一个掩码,告诉洗牌操作应将旧寄存器(R1)中的哪些字节复制到新寄存器的哪个位置。

掩码应该长这样(源:R1中的字节位置, 目标:R2中的字节位置):

{(0,0),(1,1),(1,4),(2,5),...}

这意味着几个字节将被复制两次。

我不确定我应该使用哪个函数来完成此操作。我尝试使用这两个AVX函数,第二个只使用了2个通道。

__m256 _mm256_permute_ps (__m256 a, int imm8)
__m256 _mm256_shuffle_ps (__m256 a, __m256 b, const int imm8)

我完全不了解imm8中的Shuffle Mask以及如何设计它,使其按照上述要求工作。

我查阅了这个幻灯片(第26页)中_MM_SHUFFLE有所描述,但是我找不到解决方案。

是否有任何关于如何设计这样一个掩码的教程?或者示例函数来深入理解这两种方法?

提前感谢您的提示。

1个回答

12
TL:DR: 你可能需要多次洗牌来处理车道交叉,或者如果你的模式完全像那样,你可以使用_mm256_cvtepu16_epi32vpmovzxwd),然后使用_mm256_blend_epi16
对于x86洗牌(像大多数SIMD指令集一样),目标位置是隐含的。 洗牌控制常量只有按目标顺序排列的源索引,无论它是一个编译+组装成汇编指令的imm8还是一个向量,其中每个元素都有一个索引。
每个目标位置只读取一个源位置,但同一源位置可以被多次读取。每个目标元素从洗牌源获取一个值。
请参见Convert _mm_shuffle_epi32 to C expression for the permutation?以获取dst = _mm_shuffle_epi32(src,_MM_SHUFFLE(d,c,b,a))的纯C版本,展示了如何使用控制字节。
(对于pshufb / _mm_shuffle_epi8,高位设置为零的元素会将该目标位置清零,而其他x86洗牌则忽略洗牌控制向量中的所有高位。)
没有AVX512合并掩码,就没有混洗也能混合到目标的操作。有一些双源混洗操作,如_mm256_shuffle_ps (vshufps),可以将两个来源的元素混洗在一起以生成单个结果向量。如果您想留下一些未写入的目标元素,您可能需要先进行混洗,然后再进行混合,例如使用_mm256_blendv_epi8,或者如果您可以使用16位粒度混合,则可以使用更有效的立即混合_mm256_blend_epi16,甚至更好的是_mm256_blend_epi32(在英特尔CPU上,AVX2 vpblendd_mm256_and_si256的成本相同,并且是最佳选择,如果确实需要混合,则可以完成工作;请参见http://agner.org/optimize/)。
对于你的问题(在Cannonlake中没有AVX512VBMI vpermb),你无法使用单个操作将低16“lane”的单个字节洗牌到__m256i向量的高16“lane”中。 AVX洗牌不像完整的256位SIMD,它们更像并行的两个128位操作。唯一的例外是某些带有32位粒度或更大的AVX2跨越lane的洗牌,例如vpermd (_mm256_permutevar8x32_epi32)。还有AVX2版本的pmovzx / pmovsx,例如pmovzxbq将XMM寄存器的低4字节零扩展为YMM寄存器的4个qword,而不是YMM寄存器的每半部分的低2字节。这使得它在使用内存源操作数时更加有用。

但无论如何,pshufb的AVX2版本(_mm256_shuffle_epi8)会在256位向量的两个通道中进行两个独立的16x16字节洗牌。


你可能会需要像这样的东西:

// Intrinsics have different types for integer, float and double vectors
// the asm uses the same registers either way
__m256i  shuffle_and_blend(__m256i dst, __m256i src)
{
    // setr takes element in low to high order, like a C array init
    // unlike the standard Intel notation where high element is first
    const __m256i  shuffle_control = _mm256_setr_epi8(
          0,      1,  -1, -1,   1,      2, ...);
    // {(0,0),  (1,1), (zero)  (1,4), (2,5),...}  in your src,dst notation
    // Use -1 or 0x80 or anything with the high bit set
    //  for positions you want to leave unmodified in dst
   // blendv uses the high bit as a blend control, so the same vector can do double duty

    // maybe need some lane-crossing stuff depending on the pattern of your shuffle.
    __m256i  shuffled = _mm256_shuffle_epi8(src, shuffle_control);

    // or if the pattern continues, and you're just leaving 2 bytes between every 2-byte group:
    shuffled = _mm256_cvtepu16_epi32(src);  // if src is a __m128i

    __m256i  blended = _mm256_blendv_epi8(shuffled, dst, shuffle_control);
    // blend dst elements we want to keep into the shuffled src result.
    return blended;
}    

注意,第二个16字节的pshufb编号重新从0开始。 __m256i的两半可以不同,但它们不能读取另一半的元素。如果您需要在高通道中获取字节以从低通道中获取字节,则需要进行更多的混洗和混合(例如包括vinserti128vperm2i128,或者可能是一个vpermd跨越字的混洗)以将所有所需的字节放入一个16字节组中并按某种顺序排列。
(实际上,_mm256_shuffle_epi8(PSHUFB)忽略混洗索引中的4..6位,因此写入171相同,但非常误导人。它有效地执行%16,只要高位未设置。如果混洗控制向量中的最高位设置了,则将其清零。我们不需要这个功能; _mm256_blendv_epi8不关心要替换的元素的旧值)
无论如何,这个简单的两个指令的例子只适用于模式不继续的情况。如果您想要设计真正的混洗,请提出更具体的问题。
而且,我注意到您的混合模式使用了2个新字节,然后跳过了2个。如果继续这样做,您可以使用vpblendw_mm256_blend_epi16代替blendv,因为该指令在Intel CPU上只需1个uop即可运行,而不是2个。它还将使您能够使用AVX512BW vpermw,这是当前Skylake-AVX512 CPU中提供的16位洗牌,而不是可能更慢的AVX512VBMI vpermb
或者实际上,它也许会让您使用vpmovzxwd(_mm256_cvtepu16_epi32)将16位元素零扩展为32位元素,作为跨通道的洗牌。然后与dst混合。

非常感谢您提供如此详细的答案,它对我帮助很大。 - NFoerster
1
@Thorgas:感谢您的反馈,让我知道这对初学者实际上是有用的。您是否在任何部分迷失了方向,需要更清晰地表达?我将其链接到AVX标签wiki,以及[tag:sse]。 (我故意将此答案的前半部分编写为AVX洗牌的通用指南,希望它对未来读者具有其他洗牌的用处。) - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接