Permutation 为什么并行SIMD/SSE/AVX中需要permute?

Permutation 为什么并行SIMD/SSE/AVX中需要permute?,permutation,sse,simd,avx,Permutation,Sse,Simd,Avx,从我的另一个关于我的问题中,我得到了我试图测试的代码。我以前没有对SIMD做过任何事情,所以我对这种排列方式有点陌生。首先,让我们看看这段代码: __m256i const perm_mask = _mm256_set_epi32(7, 6, 3, 2, 5, 4, 1, 0); // compare the two halves of the cache line. __m256i cmp1 = _mm256_load_si256(&node->m256[0]); __m256

从我的另一个关于我的问题中,我得到了我试图测试的代码。我以前没有对SIMD做过任何事情,所以我对这种排列方式有点陌生。首先,让我们看看这段代码:

__m256i const perm_mask = _mm256_set_epi32(7, 6, 3, 2, 5, 4, 1, 0);

// compare the two halves of the cache line.
__m256i cmp1 = _mm256_load_si256(&node->m256[0]);
__m256i cmp2 = _mm256_load_si256(&node->m256[1]);

cmp1 = _mm256_cmpgt_epi32(cmp1, value); // PCMPGTD
cmp2 = _mm256_cmpgt_epi32(cmp2, value); // PCMPGTD

// merge the comparisons back together.
//
// a permute is required to get the pack results back into order
// because AVX-256 introduced that unfortunate two-lane interleave.
//
// alternately, you could pre-process your data to remove the need
// for the permute.
__m256i cmp = _mm256_packs_epi32(cmp1, cmp2); // PACKSSDW
cmp = _mm256_permutevar8x32_epi32(cmp, perm_mask); // PERMD

// finally create a move mask and count trailing
// zeroes to get an index to the next node.

unsigned mask = _mm256_movemask_epi8(cmp); // PMOVMSKB
return _tzcnt_u32(mask) / 2; // TZCNT
作者试图用评论来解释它。然而,我并没有真正了解这种排列是如何工作的,以及为什么它最终会从结果向量中“提取”想要的信息

有谁能帮助我理解TZCNT的排列、移动掩码在这段代码中是如何使用的,以及在此上下文中“打包/解包”是什么意思?我很感谢你提供的任何资源——谷歌在这个非常特殊的话题上没有那么大的帮助

英特尔将对您学习SIMD非常有价值。它非常详细地解释了这些指令中的每一条都在做什么

SSE/AVX中的“打包”基本上是两个寄存器的向下转换和合并
PACKSSDW
将两个寄存器中的32位有符号整数打包为一个寄存器中的16位有符号整数,并对值进行饱和(因此值<-32768将设置为-32768,>32767将设置为32767)

排列是对寄存器中的值重新排序的一种方式。掩码寄存器中的每个值都指定源的索引。这是必需的,因为AVX256有点“欺骗”,并将其大多数混合指令作为两个128位“通道”进行处理

128位版本的PACKSSDW执行以下操作:

r0 := SignedSaturate(a0)
r1 := SignedSaturate(a1)
r2 := SignedSaturate(a2)
r3 := SignedSaturate(a3)
r4 := SignedSaturate(b0)
r5 := SignedSaturate(b1)
r6 := SignedSaturate(b2)
r7 := SignedSaturate(b3)
您希望256位版本与所有“A”的第一个和“B”的第二个保持相同的自然顺序,如下所示:

r0 := SignedSaturate(a0)
r1 := SignedSaturate(a1)
r2 := SignedSaturate(a2)
r3 := SignedSaturate(a3)
r4 := SignedSaturate(a4)
r5 := SignedSaturate(a5)
r6 := SignedSaturate(a6)
r7 := SignedSaturate(a7)
r8 := SignedSaturate(b0)
r9 := SignedSaturate(b1)
r10 := SignedSaturate(b2)
r11 := SignedSaturate(b3)
r12 := SignedSaturate(b4)
r13 := SignedSaturate(b5)
r14 := SignedSaturate(b6)
r15 := SignedSaturate(b7)
但实际上,它是这样做的:

r0 := SignedSaturate(a0) // lane one, the low 128 bits.
r1 := SignedSaturate(a1)
r2 := SignedSaturate(a2)
r3 := SignedSaturate(a3)
r4 := SignedSaturate(b0)
r5 := SignedSaturate(b1)
r6 := SignedSaturate(b2)
r7 := SignedSaturate(b3)
r8 := SignedSaturate(a4) // lane two, the high 128 bits.
r9 := SignedSaturate(a5)
r10 := SignedSaturate(a6)
r11 := SignedSaturate(a7)
r12 := SignedSaturate(b4)
r13 := SignedSaturate(b5)
r14 := SignedSaturate(b6)
r15 := SignedSaturate(b7)
结果是,当比较一组排列整齐的值时,128位版本将保持它们的顺序,而256位版本将混合它们。排列使它们重新排列整齐

正如我在文章中提到的,您可以通过预处理节点的数组来获得逆矩阵,从而消除代码中的排列,这样256位运算的“混合”结果将其按顺序排列:

void preprocess_avx2(bnode* const node)
{
    __m256i const perm_mask = _mm256_set_epi32(3, 2, 1, 0, 7, 6, 5, 4);
    __m256i *const middle = (__m256i*)&node->i32[4];

    __m256i x = _mm256_loadu_si256(middle);
    x = _mm256_permutevar8x32_epi32(x, perm_mask);
    _mm256_storeu_si256(middle, x);
}
排序很重要,因为它接下来要做什么

该比较在16个32位值上工作,但它会为所有值生成0x0000或0xFFFF。基本上,您只有16位信息——每个值的开关位
PMOVMSKB
将输入视为32个8字节的值,并将每个值的高位(这是我们所需要的,因为所有位都相同)打包为32位
int

TZCNT
对该
int
中的尾随零位进行计数,这为具有设定位的第一个位置提供索引:该SIMD寄存器中比较大于的第一个字节的索引


(有趣的事实:
TZCNT
是对现有的
BSF
指令的一个Haswell改进,并且实际上与它共享一个编码。唯一的区别是
TZCNT
在其输入为
0
时有一个定义的寄存器输出,而
BSF
则需要进行分支。)

现在我清楚了第二部分。但是我真的不明白他们为什么要把这两个128位的数据块混在一起——你知道他们为什么这样做吗?对我来说,想要一个完全非混合的256位间隔似乎是很自然的,这很可能是为了节省CPU上的空间。通过这种方式,他们可以只实现128位版本,而将256位版本拆分为两个128位µOp。谢谢!我想我现在已经完全了解了。非常有帮助的回答:-)更多的是理解而不是性能优化-因为它已经是pwns了<代码>打包*指令移动数据的方式与
取消打包
指令交错的方式相反@CoryNelson:Intel的CPU中的执行单元实际上是256字节宽,但是如果每个128b都可以单独实现,而不需要从每个字节到所有31个其他字节的连接,那么就可以节省导线和连接。(AMD实际上将其执行单元保持在128b宽,因此256b AVX指令的运算量是ops的两倍,周期是ops的两倍。)