【问题标题】:Reverse byte order in XMM or YMM register?XMM或YMM寄存器中的反转字节顺序?
【发布时间】:2019-06-01 14:50:41
【问题描述】:

假设我想反转一个非常大的字节数组的字节顺序。我可以使用主寄存器以慢速方式执行此操作,但我想使用 XMM 或 YMM 寄存器加快速度。

有没有办法反转 XMM 或 YMM 寄存器中的字节顺序?

【问题讨论】:

  • @GSerg 评论中的链接很棒,但请注意,对于不适合 L1/L2/L3 缓存的非常大的阵列,性能可能会受到最大值的限制DRAM 和内核之间的带宽。在这种情况下,您不会从使用 SSE/AVX 指令中获得太多好处。
  • @wim 您可能仍然是因为使用标量指令,性能实际上可能不受内存限制。
  • @fuz:我还没有研究细节,但我猜想通过展开一些 64 位的加载,bswap 它,并将其存储回内存, 每个 CPU 周期。在这种情况下,我们每个 CPU 周期需要 16 字节的带宽。使用 4 GHz cpu,带宽为 64 GB/s。当今大多数 CPU 的单核 DRAM 带宽可能较低。所以我猜bswap 解决方案可能能够使单核 DRAM 带宽饱和。因此,除非数据适合 L1/L2/L3 缓存,否则我不会期望 SSE/AVX 解决方案带来太多性能提升。
  • 您是要反转整个数组(正如其他评论者所假设的那样),还是要更改单词序列(或 dwords 或 qwords)的字节顺序?

标签: assembly x86 x86-64 sse avx


【解决方案1】:

是的,使用 SSSE3 _mm_shuffle_epi8 或 AVX2 _mm256_shuffle_epi8 在 16 字节 AVX2“通道”内随机播放字节。根据 shuffle 控制向量,您可以交换字节对、反向 4 字节单元或反向 8 字节单元。或者反转所有 16 个字节。

但是vpshufb 不是车道交叉口,因此在 AVX512VBMI vpermb 之前,您不能用一条指令反转 32 个字节。 vpshufb ymm 在 YMM 向量的两个 128 位通道中执行 2 次 16 字节洗牌。

因此,如果您要对整个数组进行字节反转,而不是数组中单个 元素 的字节顺序/字节顺序,您有 3 个选项:

  • 坚持使用 128 位向量(简单且可移植,并且在当前 CPU 上可能不会变慢)。并且只需要 16 字节对齐即可获得最佳性能。
  • 使用vmovdqu / vinsert128 加载,然后vpshufb 然后32 字节存储。 (或者进行 32 字节加载并拆分 16 字节存储,但这可能不太好)。 Vectorize random init and print for BigInt with decimal digit array, with AVX2? 在 tmp 缓冲区中包含一个缓存阻塞的字节数组反转,以 8kiB 块的形式提供 fwrite
  • vpshufb 之前或之后使用vpermq 进行通道交换(在AMD 上不是很好,并且在当前的Intel 上每时钟1 个shuffle 吞吐量存在瓶颈)。但在 Ice Lake 上可能非常好(2 个 shuffle 端口)

vpshufb 在 Intel 上是一条 uop 指令,在 AMD 上是 2 条,一次处理 32 个字节的数据。

对于非常大的输入,在矢量化循环之前达到 32 或 64 字节的对齐边界可能是值得的,因此任何加载/存储都不会跨越缓存线边界。 (对于小的输入,微不足道的好处不值得额外的序言/尾声代码和分支。)


但可能更好的是在使用前只交换一个 16kiB 的块,因此当下一步读取它时,它在 L1d 缓存中仍然很热。这称为缓存阻塞。或者也许使用 128kiB 块来阻塞 L2 缓存大小。

当您从文件中读取数据时,您可能会交换块。例如以 64k 或 128k 的块执行read() 系统调用,并在内核将数据从页面缓存复制到用户空间缓冲区后,在缓存中仍然很热的情况下交换结果。或者使用mmap 对文件进行内存映射,并从中运行复制和交换循环。 (或者对于私有映射,就地交换;但这无论如何都会触发写时复制,所以没有多大好处。Linux 上的文件支持 mmap 不能使用匿名大页面)。

如果您只读取几次数据,另一种选择是即时交换;如果以后的使用仍然受内存限制,或者有空间进行 shuffle uop 而不会出现瓶颈,那么它可能不会减慢它们的运行速度。

涉及所有数据且仅进行字节交换的传递,其计算强度非常差;您希望在数据处于寄存器中时或至少在缓存中处于热状态时对数据执行更多操作。但是,如果您只进行一次字节交换,然后多次读取数据,或者以随机访问模式,或者从其他语言(如 Python 或 JavaScript)中无法有效地即时交换,那么确定做一个交换通行证。

或者,如果您要在其上进行多次 内存限制的传递,那么交换传递会很有帮助,并且额外的 shuffle 会减慢以后的每一次传递。在这种情况下,您确实希望缓存阻止交换,以便稍后传递的输入在缓存中是热的。


标量选项bswap 被限制为每个时钟周期最多 8 个字节,并且每 8 个字节需要一个单独的加载和存储指令。 (movbe 使用字节交换从内存加载会保存一条指令,但在主流 CPU 上不会微融合成单个加载+交换微指令。不过,在 Silvermont 上它是单微指令。)此外,英特尔 bswap r64是 2 微秒,所以不是很好。

这可能会使现代 CPU 上的单线程内存带宽饱和,并进行一些循环展开,但 SIMD 处理相同数据的总 uops 较少,可以让乱序执行“看到”更远的地方并开始处理即将到来的页面的 TLB 未命中例如,更快。硬件数据预取和 TLB 预取确实有很大帮助,但对于 memcpy 使用更广泛的加载/存储通常至少会稍微好一些。

vpshufb 足够便宜,基本上仍然可以像memcpy 一样执行。或者如果在适当的位置重写会更好。)

当然,如果您有任何缓存命中,即使只是 L3 缓存,SIMD 也会真正发挥作用。

【讨论】:

  • vshufb 上的文档似乎表明它需要跟随一个置换指令,可能还有一个 OR 指令。这很令人困惑。
  • @derik:您是否要对整个数组进行字节反转?通常“字节顺序”意味着字节顺序,所以我认为你只是在反转它的 2、4 或 8 字节块。但是是的,如果你想std::reverse 带有 AVX2 vpshufb 的字节数组,它会在 16 字节“通道”内进行两次单独的字节洗牌。您可能希望使用vmovdqu [rdi+16] + vextracti128 [rdi+0], ymm0, 1 进行存储。或者使用vmovdqu + vinserti128 进行加载,这可能会更好。
  • 如果您根本不关心 AMD CPU,另一种选择是 vperm2i128vpermq 对 YMM 寄存器进行通道交换,这样您就可以进行 256b 加载+存储。但是在 Ryzen2 之前的 AMD 上,穿越车道的 256n shuffle 成本更高。作为vinserti128 一部分的额外负载uop 可能不会损害英特尔的吞吐量;在 IceLake 之前,您仍然会成为 shuffle 吞吐量的瓶颈,因此每个时钟 1 次 shuffle = 每 2 个时钟 1x 256b 存储。
  • @Noah:好的,那你当然只想要vpshufb。它需要一个常量,但您可以在循环中重用它。并且它具有出色的整体吞吐量(最坏情况下为 1/clock,或在 Ice Lake 和 Zen2 上为 YMM 版本为 2/clock。)请记住,它只关心索引的低 4 位,因此您可以广播负载相同的 16 字节内通道模式。 (除非 C 编译器进行常量传播并愚蠢地制作更宽的常量。)您可以编写像 (30,31, ... 这样的东西,但这可能会产生误导,但它很有帮助。
  • @Noah:如果周围的代码(或其他超线程)是高吞吐量的,那么一些常量传播是有意义的。请记住,OoO exec 将与之前或之后的代码重叠这一点将会发生。但是,是的,编译器在不断传播时很容易过度使用它。我没有详细查看您的案例,但是从其他负载中生成至少一些向量可能会很好。有时拥有更多控制权会很好,而不必做像static const volatile uint8_t shuf[] 这样可怕的事情并从中加载。
【解决方案2】:

我无法与传奇的 Peter Cordes 竞争...我想展示 C 实现。

这里是使用 C 内部函数反转字节顺序的示例(可用于字节反转整个数组)。

有 3 个代码示例。

  1. 使用 SSE2 指令集。
  2. 使用 SSSE3 指令集。
  3. 使用 AVX2 指令集。

//Initialize XMM register with uint8 values 0 to 15 (for testing):
__m128i a_F_E_D_C_B_A_9_8_7_6_5_4_3_2_1_0 = _mm_set_epi8(15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0);


//SSE2:
//Advantage: No need to build a shuffle mask (efficient for very short loops).
//////////////////////////////////////////////////////////////////////////
//Reverse order of uint32:
__m128i a_3_2_1_0_7_6_5_4_B_A_9_8_F_E_D_C = _mm_shuffle_epi32(a_F_E_D_C_B_A_9_8_7_6_5_4_3_2_1_0, _MM_SHUFFLE(0, 1, 2, 3));

//Swap pairs of uint16:
__m128i a_1_0_3_2_5_4_7_6_9_8_B_A_D_C_F_E = _mm_shufflehi_epi16(_mm_shufflelo_epi16(a_3_2_1_0_7_6_5_4_B_A_9_8_F_E_D_C, _MM_SHUFFLE(2, 3, 0, 1)), _MM_SHUFFLE(2, 3, 0, 1));

//Swap pairs of uint8:
__m128i a_0_1_2_3_4_5_6_7_8_9_A_B_C_D_E_F = _mm_or_si128(_mm_slli_epi16(a_1_0_3_2_5_4_7_6_9_8_B_A_D_C_F_E, 8), _mm_srli_epi16(a_1_0_3_2_5_4_7_6_9_8_B_A_D_C_F_E, 8));
//////////////////////////////////////////////////////////////////////////


//SSSE3: 
//Advantage: Not requires AVX2 support
//////////////////////////////////////////////////////////////////////////
//Build shuffle mask
const __m128i shuffle_mask = _mm_set_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15);

a_0_1_2_3_4_5_6_7_8_9_A_B_C_D_E_F = _mm_shuffle_epi8(a_F_E_D_C_B_A_9_8_7_6_5_4_3_2_1_0, shuffle_mask);
//////////////////////////////////////////////////////////////////////////


//AVX2: 
//Advantage: Potentially faster than SSSE3
//////////////////////////////////////////////////////////////////////////
//Initialize YMM register with uint8 values 0 to 31 (for testing):
__m256i a__31_to_0 = _mm256_set_epi8(31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0);

//Build shuffle mask
const __m256i shuffle_mask2 = _mm256_set_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15);

//Reverse bytes oreder of upper lane and lower lane of YMM register.
__m256i a__16_to_31__0_to_15 = _mm256_shuffle_epi8(a__31_to_0, shuffle_mask2);

//Swap upper and lower lane of YMM register
__m256i a__0_to_31 = _mm256_permute4x64_epi64(a__16_to_31__0_to_15, _MM_SHUFFLE(1, 0, 3, 2));
//////////////////////////////////////////////////////////////////////////

【讨论】:

  • 哎呀,我在__m256i a__31_to_0 和随机控制向量之间搞混了。我以为您使用_mm_setr_epi8(31, 30, ...) 进行随机控制,但这是要随机播放的数据。我还想说_mm256_shuffle_epi8 不是车道交叉口,不是set。无论如何,没关系。
猜你喜欢
  • 2013-10-22
  • 1970-01-01
  • 1970-01-01
  • 2019-12-10
  • 2017-03-30
  • 2011-05-29
  • 1970-01-01
相关资源
最近更新 更多