为啥并行 SIMD/SSE/AVX 需要置换?

Posted

技术标签:

【中文标题】为啥并行 SIMD/SSE/AVX 需要置换?【英文标题】:Why is permute needed in parallel SIMD/SSE/AVX ?为什么并行 SIMD/SSE/AVX 需要置换? 【发布时间】:2014-01-04 09:03:41 【问题描述】:

从我关于"Using SIMD AVX SSE for tree traversal" 的另一个问题中,我得到了我试图进行基准测试的这段代码。我以前没有用 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

作者Cory Nelson试图用cmets来解释。但是,我并没有真正了解这种排列是如何工作的,以及为什么它最终会从结果向量中“提取”想要的信息。

有人能帮我理解如何在此代码中使用排列、移动掩码和 TZCNT 以及“打包/解包”在此上下文中的含义吗?对于您可能拥有的任何资源,我将不胜感激 - 谷歌对这个非常特殊的主题没有帮助。

【问题讨论】:

【参考方案1】:

英特尔的instruction set manuals 对您学习 SIMD 非常宝贵。它非常详细地解释了每条指令的作用。

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

置换是一种对寄存器中的值进行重新排序的方法。掩码寄存器中的每个值都指定了源的索引。这是必需的,因为 AVX256 “作弊”了一点,并将其大部分混合指令作为两个 128 位“通道”处理。

PACKSSDW 的 128 位版本执行此操作:

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

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

(有趣的事实:TZCNT 是对现有 BSF 指令的 Haswell 改进,实际上与它共享编码。唯一的区别是 TZCNT 在其输入为 @987654334 时具有定义的寄存器输出@ -- 使用BSF 你需要分支。)

【讨论】:

第二部分现在我很清楚了。但是我真的不明白他们将两个 128 位块混合在一起的更深层原因 - 你知道他们为什么这样做吗?对我来说,想要一个完全非混合的 256 位间隔的位似乎很自然。 这可能是一个节省 CPU 空间的决定。这样他们就可以摆脱只实现 128 位版本,而将 256 位版本拆分为两个 128 位微操作。 谢谢!我想我现在已经全面了解了。非常有帮助的答案 :-) 它更多的是关于理解而不是性能优化 - 因为它已经 pwns 了! pack* 指令移动数据的方式与unpck 指令交错的方式相反。 @CoryNelson:英特尔 CPU 中的执行单元实际上是 256 位宽,但是如果每个 128b 可以单独实现,而不需要从每个字节到所有其他 31 个字节的连接,它可以节省电线和连接。 (AMD 实际上确实将它们的执行单元保持在 128b 宽,因此 256b AVX 指令的操作数是两倍,周期是两倍。)

以上是关于为啥并行 SIMD/SSE/AVX 需要置换?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我的文件加载线程没有与主线程并行?

为啥这种 for 循环并行化在 Python 中不起作用?

为啥迭代置换生成器比递归慢?

为啥 Apache Spark 的功能不并行?

为啥我的 Minitest 测试没有并行运行?

为啥执行计划中的并行性不好[关闭]