使用 ymm 寄存器作为“类似内存”的存储位置

2024-02-19

考虑 x86 中的以下循环:

; on entry, rdi has the number of iterations
.top:
; some magic happens here to calculate a result in rax
mov [array + rdi * 8], rax ; store result in output array
dec rdi
jnz .top

很简单:计算结果为rax(未显示)然后我们将结果存储到一个数组中,以与索引相反的顺序存储rdi.

我想转换上面的循环而不对内存进行任何写入(我们可以假设未显示的计算不会写入内存)。

只要循环计数在rdi是有限的,我可以使用由提供的充足空间(512字节)ymmregs 来保存值,但实际执行此操作似乎很尴尬,因为您无法“索引”任意寄存器。

一种方法是始终对整个“数组”进行洗牌ymm注册一个元素,然后将该元素插入到新释放的位置。

像这样的东西:

vpermq  ymm3, ymm3, 10_01_00_11b ; left rotate ymm by qword
vpermq  ymm2, ymm2, 10_01_00_11b ; left rotate ymm by qword
vpermq  ymm1, ymm1, 10_01_00_11b ; left rotate ymm by qword
vpermq  ymm0, ymm0, 10_01_00_11b ; left rotate ymm by qword

vblenddd ymm3, ymm3, ymm2, 3     ; promote one qword of ymm2 to ymm3
vblenddd ymm2, ymm2, ymm1, 3     ; promote one qword of ymm1 to ymm2
vblenddd ymm1, ymm1, ymm0, 3     ; promote one qword of ymm0 to ymm1

pinsrq   xmm0, rax, 0  ; playing with mixed-VEX mode fire (see Peter's answer)

这表明仅处理 16 个寄存器中的 4 个,因此显然要处理所有 16 个寄存器将需要大量代码(32 条指令)。

有没有更好的办法?

不可预测的分支是不可取的,但我们仍然可以考虑使用它们的解决方案。


你不能vpinsrq http://felixcloutier.com/x86/PINSRB:PINSRD:PINSRQ.html进入 YMM 寄存器。只有 xmm 目标可用,因此它不可避免地将整个 YMM 寄存器的上通道清零。它是随 AVX1 作为 128 位指令的 VEX 版本一起引入的。 AVX2 和 AVX512 未将其升级到 YMM/ZMM 目的地。我猜他们不想提供插入到高通道的功能,并且提供仍然只查看 imm8 最低位的 YMM 版本会很奇怪。

你将需要一个临时寄存器,然后混合到一个 YMM 中vpblendd. 或者(在 Skylake 或 AMD 上)使用旧版 SSE 版本保持高位字节不变!在 Skylake 上,使用旧版 SSE 指令编写 XMM 寄存器会对完整寄存器产生错误的依赖关系。你want这种虚假的依赖。 (我还没有测试过这个;它可能会触发某种合并微指令)。但你不希望在 Haswell 上这样做,它会保存所有 YMM 规则的上半部分,进入“状态 C”。

显而易见的解决方案是给自己留一个临时注册表以用于vmovq+vpblendd(代替vpinsrq y,r,0)。这仍然是 2 uop,但是vpblendd在 Intel CPU 上不需要端口 5,以防万一。 (movq使用端口 5)。如果你真的没有足够的空间,mm0..7MMX 寄存器可用。


降低成本

通过嵌套循环,我们可以拆分工作。通过少量展开内循环,我们基本上可以消除这部分成本。

例如,如果我们有一个内部循环产生 4 个结果,我们可以在内循环中的 2 个或 4 个寄存器上使用暴力堆栈方法,从而提供适度的开销,而无需实际展开(“神奇”有效负载仅出现一次)。 3 或 4 个 uops,可选择不带环路传送的 dep 链。

; on entry, rdi has the number of iterations
.outer:
    mov       r15d, 3
.inner:
; some magic happens here to calculate a result in rax

%if  AVOID_SHUFFLES
    vmovdqa   xmm3, xmm2
    vmovdqa   xmm2, xmm1
    vmovdqa   xmm1, xmm0
    vmovq     xmm0, rax
%else
    vpunpcklqdq  xmm2, xmm1, xmm2        ; { high=xmm2[0], low=xmm1[0] }
    vmovdqa   xmm1, xmm0
    vmovq     xmm0, rax
%endif

    dec   r15d
    jnz   .inner

    ;; Big block only runs once per 4 iters of the inner loop, and is only ~12 insns.
    vmovdqa  ymm15, ymm14
    vmovdqa  ymm13, ymm12
    ...
    
    ;; shuffle the new 4 elements into the lowest reg we read here (ymm3 or ymm4)

%if  AVOID_SHUFFLES       ; inputs are in low element of xmm0..3
    vpunpcklqdq  xmm1, xmm1, xmm0     ; don't write xmm0..2: longer false dep chain next iter.  Or break it.
    vpunpcklqdq  xmm4, xmm3, xmm2
    vinserti128  ymm4, ymm1, xmm4, 1  ; older values go in the top half
    vpxor        xmm1, xmm1, xmm1     ; shorten false-dep chains

%else                     ; inputs are in xmm2[1,0], xmm1[0], and xmm0[0]
    vpunpcklqdq  xmm3, xmm0, xmm1     ; [ 2nd-newest,  newest ]
    vinserti128  ymm3, ymm2, xmm3, 1
    vpxor        xmm2, xmm2,xmm2   ; break loop-carried dep chain for the next iter
    vpxor        xmm1, xmm1,xmm1   ; and this, which feeds into the loop-carried chain
%endif

    sub   rdi, 4
    ja   .outer

额外奖励:这只需要 AVX1(并且在 AMD 上更便宜,将 256 位向量保留在内部循环之外)。我们仍然获得 12 x 4 qwords 的存储空间,而不是 16 x 4。无论如何,这是一个任意数字。

有限展开

我们可以只展开内部循环,如下所示:

.top:
    vmovdqa     ymm15, ymm14
    ...
    vmovdqa     ymm3, ymm2           ; 12x movdqa
    vinserti128 ymm2, ymm0, xmm1, 1

    magic
    vmovq       xmm0, rax
    magic
    vpinsrq     xmm0, rax, 1
    magic
    vmovq       xmm1, rax
    magic
    vpinsrq     xmm1, rax, 1

    sub         rdi, 4
    ja          .top

当我们离开循环时,ymm15..2andxmm1和0充满了有价值的数据。如果它们位于底部,它们会运行相同的次数,但 ymm2 将是 xmm0 和 1 的副本。jmp进入循环而不执行vmovdqa第一个 iter 上的东西是一个选项。

Per 4x magic,端口 5 (movq +pinsrq) 需要 6 uops,12vmovdqa(无执行单元)和 1x vinserti128(又是端口 5)。所以每 4 个 19 个微指令magic,或 4.75 uop。

您可以交错vmovdqa + vinsert与第一个magic,或者只是在第一个之前/之后将其分开magic。你不能破坏 xmm0 直到vinserti128,但是如果你有一个空闲的整数寄存器,你可以延迟vmovq.

更多嵌套

另一个循环嵌套级别,或另一个展开, 会大大减少vmovdqa指示。不过,将数据重新整理到 YMM 规则中的成本是最低的。从 GP regs 加载 xmm https://stackoverflow.com/questions/50779309/loading-an-xmm-from-gp-regs.

AVX512可以给我们更便宜的int->xmm。 (它允许写入 YMM 的所有 4 个元素)。但我不认为它避免了展开或嵌套循环的需要,以避免每次都触及所有寄存器。


PS:

我对洗牌累加器的第一个想法是将元素向左洗牌。但后来我意识到这最终得到了 5 个状态元素,而不是 4 个,因为我们在两个寄存器中有高点和低点,加上新编写的 xmm0。 (并且可以使用 vpalignr。)

离开此处作为您可以做什么的示例vshufpd:在一个寄存器中从低位移至高位,并合并另一寄存器中的高位作为新的低位。

    vshufpd   xmm2, xmm1,xmm2, 01b     ; xmm2[1]=xmm2[0], xmm2[0]=xmm1[1].  i.e. [ low(xmm2), high(xmm1) ]
    vshufpd   xmm1, xmm0,xmm1, 01b
    vmovq     xmm0, rax

AVX512:将向量索引为内存

对于将向量寄存器写入内存的一般情况,我们可以vpbroadcastq zmm0{k1}, rax并对其他重复zmm注册到不同的k1面具。具有合并掩码(其中掩码具有单个位集)的广播为我们提供了向量寄存器中的索引存储,但我们需要针对每个可能的目标寄存器的一条指令。

创建蒙版:

xor      edx, edx
bts      rdx, rcx          #  rdx = 1<<(rcx&63)
kmovq     k1, rdx
kshiftrq  k2, k1, 8
kshiftrq  k3, k1, 16
...

To read从 ZMM 寄存器:

vpcompressq zmm0{k1}{z}, zmm1    ; zero-masking: zeros whole reg if no bits set
vpcompressq zmm0{k2},    zmm2    ; merge-masking
... repeat as many times as you have possible source regs

vmovq       rax, zmm0

(请参阅文档vpcompressq http://felixcloutier.com/x86/VPCOMPRESSQ.html:使用零掩码会将其写入的元素上方的所有元素归零)

要隐藏 vpcompressq 延迟,您可以将多个 dep 链放入多个 tmp 向量,然后vpor xmm0, xmm0, xmm1在最后。 (其中一个向量将全为零,另一个向量将具有选定的元素。)

在 SKX 上,它具有 3c 延迟和 2c 吞吐量,根据这个 instatx64 报告 https://github.com/InstLatx64/InstLatx64/blob/master/GenuineIntel0050654_SkylakeX_InstLatX64.txt.

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

使用 ymm 寄存器作为“类似内存”的存储位置 的相关文章

  • 从 exe 文件中获取汇编级代码?

    我当时正在做linux汇编编程 在过去的几天里我已经转而学习windows汇编编程 我在用ml作为我的汇编器和golink作为我的链接器 我有我的汇编代码并已获得我的exe从中 现在我需要取回它的十六进制 xff xab x55等等 在li
  • 如何在 Debian 上编译 DOS 程序?

    在我的汇编语言课程中 我们使用 DPMI 编写 DOS 程序 不幸的是 我无法一直使用 32 位 Windows 机器 我在我使用的几乎每台计算机上都安装了 Debian 虚拟机 我已经安装了 DOSBox 和 DOSEMU 有什么办法可以
  • “mov (%ebx,%eax,4),%eax”如何工作? [复制]

    这个问题在这里已经有答案了 一直在从事装配作业 并且在很大程度上我对装配非常了解 或者至少对于这项任务来说足够好 但这个 mov 的声明让我很困惑 如果有人能解释这个 mov 语句如何操作寄存器值 我将非常感激 mov ebx eax 4
  • 使用 NEON 优化 Cortex-A8 颜色转换

    我目前正在执行颜色转换例程 以便从 YUY2 转换为 NV12 我有一个相当快的函数 但没有我预期的那么快 主要是由于缓存未命中 void convert hd uint8 t orig uint8 t result uint32 t wi
  • 阴影空间示例

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

    我不明白这段代码 mov ax 07C0h Set up 4K of stack space above buffer add ax 544 8k buffer 512 paragraphs 32 paragraphs loader cli
  • 如何编译GCC生成的asm?

    我正在玩一些汇编代码 有些事情困扰着我 我编译这个 include
  • Visual Studio 2012 本机 C++ DLL x86 编译

    我最近将我的工具集从 Win 7 x86 Visual Studio 2010 升级到 Win 8 x64 Visual Studio 2012 但是 现在我的本机 C dll 编译为 x64 而不是 x86 除了将代码移至新操作系统并将其
  • 在 x86 程序集中存储大量布尔值的最佳方法是什么?

    最近我一直在处理充满布尔值的大型数组 目前 我将它们存储在 bss部分有一个 space指令 它允许我创建字节数组 但是 由于我只需要存储布尔值 因此我希望从数组中逐位读取和写入数据 目前 我能想到的最好方法是有一个 space指令所需存储
  • 如何使用 Bochs 运行汇编代码?

    我想使用 Bochs 作为 8086 模拟器 是否有捷径可寻 我想要的是类似 emu8086 的东西 http www emu8086 com http www emu8086 com 如果程序的初始部分适合 512 字节 并且您不介意将自
  • 错误 LNK2019:函数 main 中引用的外部符号无法解析

    我正在尝试在 C 中运行我的简单汇编代码 我只有两个文件 cpp 文件和 asm 文件 编译时出现错误 见下文 如果有人可以提供帮助 我将不胜感激 这是我的 main cpp 文件 include
  • 使用 AVX/AVX2 转置 8x8 浮点

    转置 8x8 矩阵可以通过制作四个 4x4 矩阵并对每个矩阵进行转置来实现 这不是我想要的 在另一个问题中 一个答案给出了解决方案 https stackoverflow com a 2518670 4144148x8 矩阵只需要 24 条
  • 为什么在展开的 ADD 循环内重新初始化寄存器会使其运行速度更快,即使循环内有更多指令?

    我有以下代码 include
  • 32 位到 64 位内联汇编移植

    我有一段 C 代码 在 GNU Linux 环境下用 g 编译 它加载一个函数指针 它如何执行并不重要 使用一些内联汇编将一些参数推送到堆栈上 然后调用该函数 代码如下 unsigned long stack 1 23 33 43 save
  • 为什么X86中没有NAND、NOR和XNOR指令?

    它们是您可以在计算机上执行的最简单的 指令 之一 它们是我亲自实施的第一个指令 执行 NOT AND x y 会使执行时间和依赖链长度和代码大小加倍 BMI1 引入了 andnot 这是一个有意义的补充 是一个独特的操作 为什么不是这个问题
  • movzbl(%rdi, %rcx, 1), %ecx 在 x86-64 汇编中意味着什么?

    我想我明白 movzbl rdi rcx 1 ecx 意思是 将零扩展字节移至长整型 并表示将 ecx 扩展为 32 位 但我不完全确定语法 rdi rcx 1 指的是什么 我在某处看到该语法指的是 Base Index Scale 但我找
  • Grub 和进入实模式(低级汇编语言编程)

    我一直在开发一个玩具操作系统 并一直使用 grub 作为我的引导加载程序 最近尝试使用 VGA 时 我发现无法使用硬件中断 我发现这是因为我被 grub 置于保护模式 有人知道如何在不删除 grub 的情况下回到实模式吗 如果您使用 GRU
  • 无法识别的仿真模式:MinGW32 上的 elf_i386

    我正在尝试制作内核 但无法链接C与程序集一起输出 这ld 我收到错误 无法识别的仿真模式 elf i386 我正在使用 Windows 10 专业版以及 MinGW32 和 MSYS 我正在使用的代码 link ld link ld OUT
  • 将字段中的位扩展到掩码中所有(重叠+相邻)集位的最快方法?

    假设我有 2 个名为 IN 和 MASK 的二进制输入 实际字段大小可能是 32 到 256 位 具体取决于用于完成任务的指令集 每次调用时两个输入都会改变 Inputs IN 1100010010010100 MASK 000111101
  • Clang 编译器 (x86):80 位长双精度

    我正在尝试在 x86 Windows 平台上使用本机 80 位长双精度 海湾合作委员会选项 mlong double 80 https gcc gnu org onlinedocs gcc x86 Options html似乎不适用于 cl

随机推荐