23

我正在编写一个密码学程序,核心(一个宽乘法例程)是用 x86-64 汇编编写的,既是为了速度,又是因为它广泛使用adc了从 C 语言不容易访问的指令。我不想内联这个函数,因为它很大并且在内部循环中被调用了好几次。

理想情况下,我还想为这个函数定义一个自定义调用约定,因为它在内部使用所有寄存器(除了rsp),不会破坏它的参数,并在寄存器中返回。现在,它已适应 C 调用约定,但这当然会使其速度变慢(大约 10%)。

为了避免这种情况,我可以调用它,asm("call %Pn" : ... : my_function... : "cc", all the registers);但有没有办法告诉 GCC 调用指令与堆栈混淆?否则 GCC 只会将所有这些寄存器放在红色区域中,而顶部的寄存器将被破坏。我可以用 -mno-red-zone 编译整个模块,但我更喜欢告诉 GCC,比如说,红色区域的前 8 个字节将被破坏,这样它就不会在那里放任何东西。

4

5 回答 5

5

From your original question I did not realize gcc limited red-zone use to leaf functions. I don't think that's required by the x86_64 ABI, but it is a reasonable simplifying assumption for a compiler. In that case you only need to make the function calling your assembly routine a non-leaf for purposes of compilation:

int global;

was_leaf()
{
    if (global) other();
}

GCC can't tell if global will be true, so it can't optimize away the call to other() so was_leaf() is not a leaf function anymore. I compiled this (with more code that triggered stack usage) and observed that as a leaf it did not move %rsp and with the modification shown it did.

I also tried simply allocating more than 128 bytes (just char buf[150]) in a leaf but I was shocked to see it only did a partial subtraction:

    pushq   %rbp
    movq    %rsp, %rbp
    subq    $40, %rsp
    movb    $7, -155(%rbp)

If I put the leaf-defeating code back in that becomes subq $160, %rsp

于 2011-06-18T19:32:10.077 回答
2

难道你不能通过在进入你的函数时将堆栈指针移动 128 个字节来修改你的汇编函数以满足 x86-64 ABI 中信号的要求吗?

或者,如果您指的是返回指针本身,请将移位放入您的调用宏中(so sub %rsp; call...

于 2011-06-17T22:38:16.810 回答
2

最大性能的方法可能是在 asm 中编写整个内部循环(包括call指令,如果它真的值得展开但不是内联。如果完全内联导致其他地方太多 uop-cache 未命中,这当然是合理的)。

无论如何,让 C 调用一个包含优化循环的 asm 函数。

顺便说一句,破坏所有寄存器会使 gcc 很难创建一个非常好的循环,因此您很可能会自己优化整个循环。(例如,可能在寄存器中保留一个指针,在内存中保留一个端点,因为cmp mem,reg仍然相当有效)。

查看代码 gcc/clang 环绕asm修改数组元素的语句(在Godbolt上):

void testloop(long *p, long count) {
  for (long i = 0 ; i < count ; i++) {
    asm("  #    XXX  asm operand in %0"
    : "+r" (p[i])
    :
    : // "rax",
     "rbx", "rcx", "rdx", "rdi", "rsi", "rbp",
      "r8", "r9", "r10", "r11", "r12","r13","r14","r15"
    );
  }
}

#gcc7.2 -O3 -march=haswell

    push registers and other function-intro stuff
    lea     rcx, [rdi+rsi*8]      ; end-pointer
    mov     rax, rdi
   
    mov     QWORD PTR [rsp-8], rcx    ; store the end-pointer
    mov     QWORD PTR [rsp-16], rdi   ; and the start-pointer

.L6:
    # rax holds the current-position pointer on loop entry
    # also stored in [rsp-16]
    mov     rdx, QWORD PTR [rax]
    mov     rax, rdx                 # looks like a missed optimization vs. mov rax, [rax], because the asm clobbers rdx

         XXX  asm operand in rax

    mov     rbx, QWORD PTR [rsp-16]   # reload the pointer
    mov     QWORD PTR [rbx], rax
    mov     rax, rbx            # another weird missed-optimization (lea rax, [rbx+8])
    add     rax, 8
    mov     QWORD PTR [rsp-16], rax
    cmp     QWORD PTR [rsp-8], rax
    jne     .L6

  # cleanup omitted.

clang 将一个单独的计数器向下计数至零。但它使用 load / add -1 / store 而不是 memory-destination add [mem], -1/ jnz

如果您自己在 asm 中编写整个循环而不是将热循环的那一部分留给编译器,您可能会做得比这更好。

如果可能,考虑使用一些 XMM 寄存器进行整数运算,以减少整数寄存器上的寄存器压力。在 Intel CPU 上,在 GP 和 XMM 寄存器之间移动只需要 1 个 ALU uop 和 1c 延迟。(在 AMD 上它仍然是 1 uop,但延迟更高,尤其是在 Bulldozer 系列上)。在 XMM 寄存器中做标量整数的事情并没有更糟,如果总 uop 吞吐量是您的瓶颈,或者它节省的溢出/重新加载比成本多,那么它可能是值得的。

但是,对于循环计数器(//// 或//与/ jcc相比paddd不太好),或者对于指针,或者对于扩展精度算术(通过比较和进位手动进行进位),XMM 当然不是很可行即使在 64 位整数 reg 不可用的 32 位模式下,也很糟糕)。如果您在加载/存储微指令上没有遇到瓶颈,通常最好将溢出/重新加载到内存而不是 XMM 寄存器。pcmpeqpmovmskbcmpjccpsubdptestjccsub [mem], 1paddq


如果您还需要从循环外部调用该函数(清理或其他),请编写一个包装器或用于add $-128, %rsp ; call ; sub $-128, %rsp在这些版本中保留红色区域。(请注意,它-128可以编码为 animm8+128不是。)

但是,在 C 函数中包含实际的函数调用并不一定可以安全地假设红色区域未使用。(编译器可见)函数调用之间的任何溢出/重新加载都可能使用红色区域,因此破坏asm语句中的所有寄存器很可能会触发该行为。

// a non-leaf function that still uses the red-zone with gcc
void bar(void) {
  //cryptofunc(1);  // gcc/clang don't use the redzone after this (not future-proof)

  volatile int tmp = 1;
  (void)tmp;
  cryptofunc(1);  // but gcc will use the redzone before a tailcall
}

# gcc7.2 -O3 output
    mov     edi, 1
    mov     DWORD PTR [rsp-12], 1
    mov     eax, DWORD PTR [rsp-12]
    jmp     cryptofunc(long)

如果您想依赖编译器特定的行为,您可以在热循环之前调用(使用常规 C)非内联函数。使用当前的 gcc / clang,这将使他们保留足够的堆栈空间,因为无论如何他们都必须调整堆栈(rsp在 a 之前对齐call)。这根本不是面向未来的,但应该会起作用。


GNU C 有一个__attribute__((target("options")))x86 函数属性,但它不能用于任意选项,并且-mno-red- zone不是您可以在每个函数的基础上或#pragma GCC target ("options")在编译单元内切换的选项之一。

你可以使用类似的东西

__attribute__(( target("sse4.1,arch=core2") ))
void penryn_version(void) {
  ...
}

但不是__attribute__(( target("mno-red-zone") ))

有一个#pragma GCC optimize和一个optimize函数属性(两者都不适用于生产代码),但#pragma GCC optimize ("-mno-red-zone")也不起作用。我认为这个想法是让一些重要的功能-O2即使在调试版本中也能得到优化。您可以设置-f选项或-O.

不过,您可以将函数本身放在一个文件中,然后使用 编译该编译单元-mno-red-zone。(希望 LTO 不会破坏任何东西……)

于 2017-11-20T22:54:29.513 回答
0

如果创建一个用 C 语言编写并且除了调用内联程序集什么都不做的虚拟函数呢?

于 2013-07-02T04:08:06.917 回答
0

不确定,但查看GCC 文档中的函数属性,我发现了stdcall可能感兴趣的函数属性。

我仍然想知道你发现你的 asm 调用版本有什么问题。如果只是美学,您可以将其转换为宏或内联函数。

于 2011-06-17T22:31:39.697 回答