【问题标题】:Why not store function parameters in XMM vector registers?为什么不将函数参数存储在 XMM 向量寄存器中?
【发布时间】:2016-02-15 21:21:11
【问题描述】:

我目前正在阅读这本书:“计算机系统 - 程序员视角”。我发现,在 x86-64 架构上,我们仅限于 6 个整数参数,这些参数将传递给寄存器中的函数。下一个参数将在堆栈上传递。

此外,第一个最多 8 个 FP 或向量 args 在 xmm0..7 中传递。

为什么不使用浮点寄存器来存储下一个参数,即使参数不是单/双精度变量?

将数据存储在寄存器中比将数据存储到内存然后从内存中读取要高效得多(据我所知)。

【问题讨论】:

  • 看起来,XMM* registers 可以类似地用于传递,但只能用于浮点参数。
  • @StuartLC 为什么不在其中存储整数数据,因为寄存器存储位。他们并不真正知道是否存储了整数或浮点数据。如果我最近将它们移动到像 %rax 这样的积分寄存器,一切都应该没问题。我认为这应该是一个性能提升,即使代码会很混乱......
  • 在猜测中,将整数编码/解码为浮点数,反之亦然,而不是在堆栈上传递。但希望专家可以回答:)
  • @StuartLC 我不想将整数转换为浮点数,然后再从浮点数转换为整数。这个想法是在不转换的情况下存储数据,以避免精度损失。如果我们将指针地址存储在浮点寄存器中,并且在转换操作之后,地址的值会被四舍五入,我们会得到分段错误,这不好:)

标签: assembly x86 parameter-passing x86-64 calling-convention


【解决方案1】:

大多数函数的整数参数不超过 6 个,所以这确实是一个极端情况。在 xmm 寄存器中传递一些多余的整数参数会使在哪里找到浮点参数的规则更加复杂,几乎没有好处。除了它可能不会让代码更快的事实。

在内存中存储多余参数的另一个原因是您的函数可能不会立即使用它们。如果你想调用另一个函数,你必须将这些参数从 xmm 寄存器保存到内存中,因为你调用的函数会破坏任何参数传递寄存器。 (无论如何,所有 xmm regs 都是调用者保存的。)因此,您最终可能会得到将参数填充到无法直接使用的向量寄存器中的代码,然后在调用另一个函数之前将它们存储到内存中,并且仅然后 将它们加载回整数寄存器。或者即使该函数不调用其他函数,也许它需要向量寄存器供自己使用,并且必须将参数存储到内存中以释放它们以运行向量代码!将push 参数放到堆栈上会更容易,因为push 非常优化,出于显而易见的原因,在单个微指令中完成存储和RSP 的修改,与mov 一样便宜.

SysV Linux/Mac x86-64 ABI (r11) 中有一个整数寄存器不用于参数传递,但也不保留调用。为惰性动态链接器代码使用临时寄存器而不保存(因为此类 shim 函数需要将其所有 args 传递给动态加载的函数)和类似的包装器函数是很有用的。

所以 AMD64 可以为函数参数使用更多的整数寄存器,但代价是调用函数在使用前必须保存的寄存器数量。 (或者用于不使用“静态链”指针或其他东西的语言的双用途 r10。)

无论如何,在寄存器中传递的参数越多并不总是越好。


xmm 寄存器不能用作指针或索引寄存器,将数据从 xmm 寄存器移回整数寄存器可能会比加载刚刚存储的数据更减慢周围的代码。 (如果任何执行资源将成为瓶颈,而不是缓存未命中或分支错误预测,则更有可能是 ALU 执行单元,而不是加载/存储单元。将数据从 xmm 移动到 gp 寄存器需要 ALU uop,在 Intel和 AMD 当前的设计。)

L1 缓存非常快,并且存储->负载转发使往返内存的总延迟大约为 5 个周期,例如英特尔哈斯韦尔。 (像inc dword [mem]这样的指令的延迟是6个周期,包括一个ALU周期。)

如果将数据从 xmm 移动到 gp 寄存器是所有您将要做的(没有其他东西可以让 ALU 执行单元保持忙碌),那么是的,在 Intel CPU 上,@ 的往返延迟987654329@/movd eax, xmm0(2 个周期 Intel Haswell)小于 mov [mem], eax/mov eax, [mem](5 个周期 Intel Haswell)的延迟,但整数代码通常不会像 FP 代码那样完全受到延迟的限制。

在 AMD Bulldozer 系列 CPU 上,两个整数内核共享一个向量/FP 单元,直接在 GP regs 和 vector regs 之间移动数据实际上非常慢(单向 8 或 10 个周期,或 Steamroller 的一半)。一次内存往返只有 8 个周期。

32 位代码设法运行得相当好,即使 所有 参数都在堆栈上传递,并且必须加载。 CPU 在将参数存储到堆栈然后再次加载它们方面进行了高度优化,因为笨拙的旧 32 位 ABI 仍然用于很多代码,尤其是。在 Windows 上。 (大多数 Linux 系统大多运行 64 位代码,而大多数 Windows 桌面系统运行大量 32 位代码,因为很多 Windows 程序只能作为预编译的 32 位二进制文​​件提供。)

请参阅http://agner.org/optimize/ 获取 CPU 微体系结构指南,了解如何计算实际需要多少个周期。 wiki 中还有其他很好的链接,包括上面链接的 x86-64 ABI 文档。

【讨论】:

  • 感谢您的详细解答!我真的很震惊,“将数据从 xmm 寄存器移回整数寄存器会比加载刚刚存储的数据要慢。”
  • @denis631:实际上这仅适用于 AMD Bulldozer 系列。我应该说“快不了多少”,它会占用 ALU 执行资源而不是加载/存储执行资源。
  • @denis631:更新解释了使用 ALU 执行单元与加载/存储单元,以及延迟与吞吐量。并再次更新指出当被调用函数想要调用其他函数时,过多的参数传递寄存器实际上会造成伤害。
  • @denis631 - 这是一个很好的答案,它解释了通过寄存器传递的成本与调用者/被调用者节省之间的平衡/压力。我在这个问答中没有看到任何提及它,但你可能想看看ABI documentation
【解决方案2】:

我认为这不是个好主意,因为:

  1. 您不能将 FPU/SSE 寄存器用作通用寄存器。我的意思是,这段代码不正确(NASM):

    mov byte[st0], 0xFF
    
  2. 如果比较向/从 FPU/SSE 发送数据与通用寄存器/内存,FPU/SSE 非常慢。

编辑:记住,我可能不对。

【讨论】:

  • 2:从 xmm/mmx/fpu 寄存器加载/存储在最坏的情况下比从 GP 寄存器加载/存储多一个周期。直接在 XMM 和 GP 寄存器之间移动数据(movd/movq/pinsrw/pextrw)在 Intel 上只是一个周期延迟,但在 AMD Bulldozer 系列上是几个周期的延迟。 (BD CPU 在一对整数内核之间共享一个向量/FPU 执行单元。当然,每个内核都有自己的 xmm/fpu 架构状态,但必须与其他内核竞争执行资源。)AMD 没有设计 BD然而,当他们制作 AMD64 ABI 时。
  • 无论如何,不​​要说“非常慢”,而是说“不比从内存加载快”。不过,您的第 1 点是正确的。
  • 也许吧。我不检查 FPU/SSE 的加载/存储,但在我的情况下,SSE 并不是很快。它慢了 5-6 倍。感谢指正
  • 什么比什么慢 5-6 倍,在什么 CPU 上?在现代 Intel CPU 上,128 位 SSE 加载/存储与 GP 寄存器加载/存储具有相同的吞吐量。 (在 Haswell 及更高版本上每个周期 1 次存储和 2 次加载)。 128 位 SSE 存储(不跨越缓存线边界)的往返存储->加载延迟为 6 个周期,而 GP 寄存器为 5 个周期。 (英特尔 SnB 和 Haswell)。来自 Agner Fog 指令表的数字。
  • SSE向量长度实现。在这种情况下,FPU 比 SSE 快 5-6 倍。但是,我是初学者,我可能会编写不正确或速度较慢的代码,但我认为这是题外话。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-07-21
  • 1970-01-01
  • 2011-01-14
  • 2015-05-25
  • 1970-01-01
  • 1970-01-01
  • 2012-06-15
相关资源
最近更新 更多