使用 x64 SIMD 进行半字节改组

2024-04-01

我知道字节改组 https://www.felixcloutier.com/x86/pshufb指令,但我想对半字节(4 位值)做同样的事情,具体来说,我想在 64 位字中混洗 16 个半字节。我的洗牌索引也存储为 16 个半字节。最有效的实施是什么?


带有必须以这种方式存储的控制向量的任意洗牌?唉,很难共事。我猜你必须将两者都解压才能提供 SSSE3pshufb然后重新打包该结果。

大概只是punpcklbw反对右移副本,然后 AND 掩码以仅保留每个字节中的低 4 位。然后pshufb.

有时,奇数/偶数分割比加宽每个元素更容易(因此位仅保留在其原始字节或字内)。在这种情况下,如果我们可以更改您的半字节索引编号,punpcklqdq可以将奇数或偶数半字节放入高半部分,准备将它们带回下方并进行“或”操作。

但如果不这样做,重新包装就是一个单独的问题。我猜想将相邻的字节对组合成低字节中的一个字,也许是pmaddubsw https://www.felixcloutier.com/x86/pmaddubsw如果吞吐量比延迟更重要。然后你可以packuswd https://www.felixcloutier.com/x86/packuswb(针对零或自身)或pshufb(具有恒定的控制向量)。

如果您要进行多次这样的洗牌,您可以将两个向量打包为一个,以便存储movhps / movq。使用 AVX2,可以让所有其他指令在两个 128 位通道中的两个独立的洗牌上工作。

// UNTESTED, requires only SSSE3
#include <stdint.h>
#include <immintrin.h>

uint64_t shuffle_nibbles(uint64_t data, uint64_t control)
{
  __m128i vd = _mm_cvtsi64_si128(data);    // movq
  __m128i vd_hi = _mm_srli_epi32(vd, 4);   // x86 doesn't have a SIMD byte shift
  vd = _mm_unpacklo_epi8(vd, vd_hi);       // every nibble at the bottom of a byte, with high garbage
  vd = _mm_and_si128(vd, _mm_set1_epi8(0x0f));  // clear high garbage for later merging

  __m128i vc = _mm_cvtsi64_si128(control);
  __m128i vc_hi = _mm_srli_epi32(vc, 4);
  vc = _mm_unpacklo_epi8(vc, vc_hi);

  vc = _mm_and_si128(vc, _mm_set1_epi8(0x0f));  // make sure high bit is clear, else pshufb zeros that element.
       //  AVX-512VBMI  vpermb doesn't have that problem, if you have it available
  vd = _mm_shuffle_epi8(vd, vc);

       // left-hand input is the unsigned one, right hand is treated as signed bytes.
  vd = _mm_maddubs_epi16(vd, _mm_set1_epi16(0x1001));  // hi nibbles << 4 (*= 0x10), lo nibbles *= 1.

  // vd has nibbles merged into bytes, but interleaved with zero bytes
  vd = _mm_packus_epi16(vd, vd);  // duplicate vd into low & high halves.
  //  Pack against _mm_setzero_si128() if you're not just going to movq into memory or a GPR and you want the high half of the vector to be zero.
  return _mm_cvtsi128_si64(vd);
}

屏蔽数据0x0f在 shuffle 之前(而不是之后)允许在具有两个 shuffle 单元的 CPU 上实现更多 ILP。至少如果它们在向量寄存器中已经有 uint64_t 值,或者数据和控制值来自内存,那么两者都可以在同一周期中加载。如果来自 GPR,则为 1/时钟吞吐量vmovq xmm, reg意味着 dep 链之间存在资源冲突,因此它们不能在同一周期中启动。但由于数据可能在控制之前就准备好了,因此提前屏蔽可以使其远离控制->输出延迟的关键路径。

如果延迟而不是通常的吞吐量成为瓶颈,请考虑更换pmaddubsw右移 4,por,和/包。或者pshufb打包,同时忽略奇数字节中的垃圾。既然你无论如何都需要另一个常量,不妨将其设为pshufb常数而不是and.

如果您有 AVX-512,则进行移位和位混合vpternlogd可以避免在洗牌之前需要屏蔽数据,并且vpermb代替vpshufb将避免需要屏蔽控件,因此您可以避免set1_epi8(0x0f)完全恒定。

clang 的 shuffle 优化器没有发现任何东西,只是像 GCC 那样编译它(https://godbolt.org/z/xz7TTbM1d https://godbolt.org/z/xz7TTbM1d), 即使-march=sapphirerapids。没有发现它可以使用vpermb代替vpand / vpshufb.

shuffle_nibbles(unsigned long, unsigned long):
        vmovq   xmm0, rdi
        vpsrld  xmm1, xmm0, 4
        vpunpcklbw      xmm0, xmm0, xmm1        # xmm0 = xmm0[0],xmm1[0],xmm0[1],xmm1[1],xmm0[2],xmm1[2],xmm0[3],xmm1[3],xmm0[4],xmm1[4],xmm0[5],xmm1[5],xmm0[6],xmm1[6],xmm0[7],xmm1[7]
        vmovq   xmm1, rsi
        vpsrld  xmm2, xmm1, 4
        vpunpcklbw      xmm1, xmm1, xmm2        # xmm1 = xmm1[0],xmm2[0],xmm1[1],xmm2[1],xmm1[2],xmm2[2],xmm1[3],xmm2[3],xmm1[4],xmm2[4],xmm1[5],xmm2[5],xmm1[6],xmm2[6],xmm1[7],xmm2[7]
        vmovdqa xmm2, xmmword ptr [rip + .LCPI0_0] # xmm2 = [15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15]
        vpand   xmm0, xmm0, xmm2
        vpand   xmm1, xmm1, xmm2
        vpshufb xmm0, xmm0, xmm1
        vpmaddubsw      xmm0, xmm0, xmmword ptr [rip + .LCPI0_1]
        vpackuswb       xmm0, xmm0, xmm0
        vmovq   rax, xmm0
        ret

(没有AVX,需要额外2个movdqa注册复制指令。)

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用 x64 SIMD 进行半字节改组 的相关文章

  • 为什么我的空循环在 Intel Skylake CPU 上作为函数调用时运行速度是原来的两倍?

    我正在运行一些测试来比较 C 和 Java 并遇到了一些有趣的事情 在 main 调用的函数中 而不是在 main 本身中 运行具有优化级别 1 O1 的完全相同的基准代码 导致性能大约翻倍 我正在打印 test t 的大小 以毫无疑问地验
  • 使用 (float&)int 进行类型双关可以正常工作,(float const&)int 会像 (float)int 一样转换吗?

    VS2019 发布 x86 template
  • 阴影空间示例

    EDIT 我接受了下面的答案 并添加了我自己的代码的最终修订版 希望它向人们展示影子空间分配的实际示例 而不是更多的文字 编辑 2 我还设法在 YouTube 视频 所有内容 的注释中找到了一个调用约定 PDF 的链接 其中有一些关于 Li
  • 什么是“矢量化”?

    现在好几次了 我在 matlab fortran 其他一些 中遇到这个术语 但我从来没有找到解释它是什么意思 它有什么作用 所以我在这里问 什么是矢量化 例如 循环矢量化 是什么意思 许多CPU具有 向量 或 SIMD 指令集 它们同时对两
  • 为什么 clang 使用 -O0 生成低效的 asm(对于这个简单的浮点和)?

    我正在 llvm clang Apple LLVM 版本 8 0 0 clang 800 0 42 1 上反汇编此代码 int main float a 0 151234 float b 0 2 float c a b printf f c
  • 在 C# 中按元素相乘数组具有意想不到的性能

    我想找到按元素相乘两个数组的最佳方法 这是更广泛项目的一部分 其中性能而不是唯一的考虑因素 我今天开始用 C Linqpad 编写一些函数 因此它还没有以任何方式进行优化 下面代码的输出如下 Environment ProcessorCou
  • SIMD 和 VLIW 指令是一样的吗?

    SIMD 单指令多数据 和 VLIW 超长指令字 到底有什么区别 其中一个是另一个的子集吗 或者它们是两个完全不同的东西 完全不相关且正交 一台机器可以有一个或两个 或者两者都没有 SIMD 指令可以作为扩展添加到 VLIW ISA 但 V
  • 为什么 printf 使用浮点和整数格式说明符打印随机值

    我在64位机器上写了一个简单的代码 int main printf d 2 443 所以 这就是编译器的行为方式 它将识别第二个参数为双精度型 因此它将在堆栈上压入 8 个字节 或者可能只是在调用之间使用寄存器来访问变量 d需要 4 字节整
  • 为什么X86中没有NAND、NOR和XNOR指令?

    它们是您可以在计算机上执行的最简单的 指令 之一 它们是我亲自实施的第一个指令 执行 NOT AND x y 会使执行时间和依赖链长度和代码大小加倍 BMI1 引入了 andnot 这是一个有意义的补充 是一个独特的操作 为什么不是这个问题
  • SSE:跨页边界的未对齐加载和存储

    我在页面边界旁边执行未对齐加载或存储之前读过某处 例如使用 mm loadu si128 mm storeu si128内在函数 代码应首先检查整个向量 在本例中为 16 个字节 是否属于同一页 如果不属于同一页 则切换到非向量指令 我知道
  • 在 Intel x86 架构上使用非 AVX 指令移动 xmm 整数寄存器值

    我有以下问题 需要使用 AVX2 以外的任何工具来解决 我有 3 个值存储在 m128i 变量中 不需要第四个值 需要将这些值移动 4 3 5 我需要两个功能 一个用于按这些值进行右逻辑移位 另一个用于左逻辑移位 有谁知道使用 SSE AV
  • 如何有效地扫描每次迭代交替的 2 位掩码

    给定 2 个位掩码 应交替访问 0 1 0 1 我尝试获得运行时高效的解决方案 但找不到比以下示例更好的方法 uint32 t mask 2 uint8 t mask index 0 uint32 t f tzcnt u32 mask ma
  • SMP 上如何处理中断?

    SMP 对称多处理器 多核 机器上如何处理中断 内存管理单元是只有一个还是多个 假设两个线程 A 和 B 运行在不同的内核上 同时 访问页表中不存在的内存页面 在这种情况下 将会出现页面错误 并从内存中引入新页面 将会发生的事件的顺序是什么
  • 为什么“+=”在 SSE 内在函数中给出了意想不到的结果

    sse内在累加有两种实现方式 但其中之一得到了错误的结果 include
  • 在 SSE 和 AVX512 寄存器之间移动数据?

    我想将四个 xmm 寄存器移动到一个 zmm 寄存器中 使用 AVX512 指令执行一些计算并将结果返回到 XMM 寄存器 不通过内存来做到这一点的最有效方法是什么 None
  • 64 位上的 ASLR 和内存布局:是否仅限于规范部分 (128 TiB)?

    当加载启用 ASLR 的 PIE 可执行文件时 Linux 是否会限制程序段到规范部分 最多 0000 7fff ffff ffff 的映射 还是会使用完整的较低部分 起始位 0 显然 Linux 不会给你的进程提供不可用的地址 这会导致它
  • 与 SSE 比较 16 字节字符串

    我有 16 字节的 字符串 它们可能更短 但您可能会假设它们在末尾用零填充 但您可能不会假设它们是 16 字节对齐的 至少不总是 如何编写一个例程将它们与 SSE 内在函数进行比较 是否相等 我发现这个代码片段可能会有帮助 但我不确定它是否
  • 段寄存器如何参与内存地址转换?

    到目前为止我所学到的有关细分的知识 虚拟地址包含段选择器和偏移量 段选择器与GDTR配合使用 查找段描述符的线性地址 段描述符保存有关所选段的信息 包括其线性地址 所以 我的问题是 根据我所读到的内容 虚拟地址被加载到段寄存器中 然后以某种
  • 从 64 位 nasm 代码接收 32 位寄存器

    我正在学习 64 位 nasm 我通过执行以下操作来汇编 nasm 文件 该文件仅包含 64 位寄存器 nasm f elf64 HelloWorld nasm o HelloWorld o 并链接它执行以下操作 ld HelloWorld
  • 使用非规范地址检索内存数据会导致 SIGSEGV 而不是 SIGBUS

    我无法使用以下汇编代码产生 总线错误 这里我使用的内存地址不是合法的 规范地址 那么 我怎样才能触发该错误呢 我在带有 NASM 2 14 02 的 Ubuntu 20 04 LTS 下运行这段代码 但它会导致负载出现 SIGSEGV 分段

随机推荐