【问题标题】:Work around windows calling convention preserving xmm registers?解决保留 xmm 寄存器的 Windows 调用约定?
【发布时间】:2019-05-17 15:06:22
【问题描述】:

在 Windows 上是否有任何方法可以解决 XMM 寄存器保留在函数调用中的要求?(除了将其全部写入汇编)

不幸的是,我有许多 AVX2 内在函数因此而臃肿。

作为一个例子,编译器(MSVC)将把它放在函数的顶部:

00007FF9D0EBC602 vmovaps xmmword ptr [rsp+1490h],xmm6
00007FF9D0EBC60B vmovaps xmmword ptr [rsp+1480h],xmm7
00007FF9D0EBC614 vmovaps xmmword ptr [rsp+1470h],xmm8
00007FF9D0EBC61D vmovaps xmmword ptr [rsp+1460h],xmm9
00007FF9D0EBC626 vmovaps xmmword ptr [rsp+1450h],xmm10
00007FF9D0EBC62F vmovaps xmmword ptr [rsp+1440h],xmm11
00007FF9D0EBC638 vmovaps xmmword ptr [rsp+1430h],xmm12
00007FF9D0EBC641 vmovaps xmmword ptr [rsp+1420h],xmm13
00007FF9D0EBC64A vmovaps xmmword ptr [rsp+1410h],xmm14
00007FF9D0EBC653 vmovaps xmmword ptr [rsp+1400h],xmm15

然后在函数的最后..

00007FF9D0EBD6E6 vmovaps xmm6,xmmword ptr [r11-10h]
00007FF9D0EBD6EC vmovaps xmm7,xmmword ptr [r11-20h]
00007FF9D0EBD6F2 vmovaps xmm8,xmmword ptr [r11-30h]
00007FF9D0EBD6F8 vmovaps xmm9,xmmword ptr [r11-40h]
00007FF9D0EBD6FE vmovaps xmm10,xmmword ptr [r11-50h]
00007FF9D0EBD704 vmovaps xmm11,xmmword ptr [r11-60h]
00007FF9D0EBD70A vmovaps xmm12,xmmword ptr [r11-70h]
00007FF9D0EBD710 vmovaps xmm13,xmmword ptr [r11-80h]
00007FF9D0EBD716 vmovaps xmm14,xmmword ptr [r11-90h]
00007FF9D0EBD71F vmovaps xmm15,xmmword ptr [r11-0A0h]

这是 20 条指令,因为我不需要保留 XMM 的状态,所以什么都不做。我有 100 个这样的函数,编译器会像这样膨胀。它们都是通过函数指针从同一个调用站点调用的。

我尝试更改调用约定(__vectorcall/cdecl/fastcall),但这似乎没有任何作用。

【问题讨论】:

  • 通常,这些内在函数是内联的。为什么你的代码不是这样?
  • 您确定编译器优化已打开吗? “虚拟机”是什么意思?如果功能不小,保存和恢复寄存器有什么问题?
  • 调用代码假定寄存器被保留。如果你不保留它们,调用函数(或者它的调用者,或者它的调用者的调用者,......)可能会出现异常。
  • @Froglegs 我不对你的心理能力做任何假设。但是鉴于您在问题中提供了零细节并且没有minimal reproducible example,我不得不猜测实际情况是什么。也许下次尝试问一个更好的问题,而不是因为别人试图帮助你而感到侮辱。
  • 大多数解释器不会执行您的代码正在执行的操作,因为从单个调用站点调用数百个可能的函数通常会导致几乎每个调用都被分支预测错误预测,从而导致巨大的停顿。因此,大多数解释器至少会尝试部分内联函数并使用诸如使用计算机 goto 的所谓“线程代码”之类的技巧来改进预测。此时担心保存这些寄存器的成本可能还为时过早。

标签: windows assembly sse calling-convention abi


【解决方案1】:

对您希望通过函数指针拼凑在一起的辅助函数使用 x86-64 System V 调用约定。在该调用约定中,所有 xmm/ymm0..15 和 zmm0..31 都被调用破坏,因此即使需要超过 5 个向量寄存器的辅助函数也不必保存/恢复任何内容。

调用它们的外部解释器函数仍应使用 Windows x64 fastcall 或 vectorcall,因此从外部看,它完全尊重调用约定。

这会将 XMM6..15 的所有保存/恢复提升到该调用者,而不是每个辅助函数。这减少了静态代码大小并通过函数指针分摊了多次调用的运行时成本。


AFAIK,MSVC 不支持将函数标记为使用 x86-64 System V 调用约定,仅支持 fastcall 与 vectorcall,因此您必须使用 clang

(ICC 有问题,无法在调用 System V ABI 函数时保存/恢复 XMM6..15)。

Windows GCC is buggy with 32-byte stack alignment 用于溢出 __m256,因此将 GCC 与 -march= 与包含 AVX 的任何内容一起使用通常是不安全的。


在函数和函数指针声明中使用__attribute__((sysv_abi))__attribute__((ms_abi))

我认为ms_abi__fastcall,而不是__vectorcall。 Clang 可能也支持__attribute__((vectorcall)),但我还没有尝试过。 Google 结果主要是功能请求/讨论。

void (*helpers[10])(float *, float*) __attribute__((sysv_abi));

__attribute__((ms_abi))
void outer(float *p) {
    helpers[0](p, p+10);
    helpers[1](p, p+10);
    helpers[2](p+20, p+30);
}

编译如下on Godbolt with clang 8.0-O3 -march=skylake。 (Godbolt 目标 Linux 上的 gcc/clang,但我在函数和函数指针上都使用了显式的 ms_abisysv_abi,因此代码生成不依赖于默认值为 sysv_abi 的事实。显然你d 想使用 Windows gcc 或 clang 构建您的函数,因此对其他函数的调用将使用正确的调用约定。以及有用的对象文件格式等)

请注意,gcc/clang 为 outer() 发出代码,该代码需要 RCX(Windows x64)中的传入指针 arg,但将其传递给 RDI 和 RSI(x86-64 System V)中的被调用方。

outer:                                  # @outer
        push    r14
        push    rsi
        push    rdi
        push    rbx
        sub     rsp, 168
        vmovaps xmmword ptr [rsp + 144], xmm15 # 16-byte Spill
        vmovaps xmmword ptr [rsp + 128], xmm14 # 16-byte Spill
        vmovaps xmmword ptr [rsp + 112], xmm13 # 16-byte Spill
        vmovaps xmmword ptr [rsp + 96], xmm12 # 16-byte Spill
        vmovaps xmmword ptr [rsp + 80], xmm11 # 16-byte Spill
        vmovaps xmmword ptr [rsp + 64], xmm10 # 16-byte Spill
        vmovaps xmmword ptr [rsp + 48], xmm9 # 16-byte Spill
        vmovaps xmmword ptr [rsp + 32], xmm8 # 16-byte Spill
        vmovaps xmmword ptr [rsp + 16], xmm7 # 16-byte Spill
        vmovaps xmmword ptr [rsp], xmm6 # 16-byte Spill
        mov     rbx, rcx                            # save p 
        lea     r14, [rcx + 40]
        mov     rdi, rcx
        mov     rsi, r14
        call    qword ptr [rip + helpers]
        mov     rdi, rbx
        mov     rsi, r14
        call    qword ptr [rip + helpers+8]
        lea     rdi, [rbx + 80]
        lea     rsi, [rbx + 120]
        call    qword ptr [rip + helpers+16]
        vmovaps xmm6, xmmword ptr [rsp] # 16-byte Reload
        vmovaps xmm7, xmmword ptr [rsp + 16] # 16-byte Reload
        vmovaps xmm8, xmmword ptr [rsp + 32] # 16-byte Reload
        vmovaps xmm9, xmmword ptr [rsp + 48] # 16-byte Reload
        vmovaps xmm10, xmmword ptr [rsp + 64] # 16-byte Reload
        vmovaps xmm11, xmmword ptr [rsp + 80] # 16-byte Reload
        vmovaps xmm12, xmmword ptr [rsp + 96] # 16-byte Reload
        vmovaps xmm13, xmmword ptr [rsp + 112] # 16-byte Reload
        vmovaps xmm14, xmmword ptr [rsp + 128] # 16-byte Reload
        vmovaps xmm15, xmmword ptr [rsp + 144] # 16-byte Reload
        add     rsp, 168
        pop     rbx
        pop     rdi
        pop     rsi
        pop     r14
        ret

GCC 编写的代码基本相同。但 Windows GCC 与 AVX 存在问题。

ICC19 编写了类似的代码,但没有 xmm6..15 的保存/恢复。这是一个引人注目的错误;如果任何被调用者确实按照允许的方式破坏了这些 reg,那么从该函数返回将违反其调用约定。

这使得 clang 成为您可以使用的唯一编译器。没关系;叮当很好。


如果您的被调用者不需要所有 YMM 寄存器,则在外部函数中保存/恢复所有这些寄存器是多余的。但是现有工具链没有中间立场。例如,您必须在 asm 中手写 outer 以利用知道您可能的被调用者都不会破坏 XMM15 的优势。


请注意,从outer() 内部调用其他 MS-ABI 函数是完全可以的。 GCC / clang 也会(排除错误)为此发出正确的代码,如果被调用的函数选择不破坏 xmm6..15 也没关系。

【讨论】:

  • 谢谢彼得,我会用 Clang 编译那部分代码
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-01-20
  • 1970-01-01
  • 1970-01-01
  • 2012-07-22
  • 2011-07-14
  • 1970-01-01
  • 2017-10-29
相关资源
最近更新 更多