内联装配破坏了红色区域

时间:2011-06-17 03:20:06

标签: c gcc x86 inline-assembly red-zone

我正在编写一个加密程序,并且核心(一个广泛的乘法例程)是用x86-64汇编编写的,这两者都是为了提高速度,因为它广泛使用了不容易从C中访问的adc之类的指令我不想内联这个函数,因为它很大,并且在内循环中被调用了几次。

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

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

5 个答案:

答案 0 :(得分:5)

从你原来的问题我没有意识到gcc限制红区使用到叶子功能。我不认为这是x86_64 ABI所要求的,但它是编译器的合理简化假设。在这种情况下,您只需要将调用汇编例程的函数设置为非叶子以进行编译:

int global;

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

GCC无法判断global是否为真,因此无法优化对other()的调用,因此was_leaf()不再是叶函数。我编译了这个(有更多的代码触发了堆栈的使用)并且观察到它作为一个叶子它没有移动%rsp并且显示它所做的修改。

我也试过在一个叶子中分配超过128个字节(只是char buf[150]),但我很震惊地看到它只进行了部分减法:

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

如果我将失败的代码放回subq $160, %rsp

答案 1 :(得分:2)

您是否可以通过在进入函数时将堆栈指针移动128个字节来修改汇编函数以满足x86-64 ABI中信号的要求?

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

答案 2 :(得分:1)

最大性能方式可能是在asm中编写整个内部循环(包括call指令,如果它真的值得展开但不能内联。如果完全内联导致它肯定是合理的在其他地方有太多uop-cache错过了。

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

BTW,clobbering 所有寄存器使得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个用户,但延迟时间更长,尤其是Bulldozer家族)。在XMM寄存器中执行标量整数的情况并不是很糟糕,如果总uop吞吐量是你的瓶颈,或者它节省了比成本更多的溢出/重新加载,那么它是值得的。

但当然XMM对循环计数器来说不太可行(paddd / pcmpeq / pmovmskb / cmp / jccpsubd / ptest / jccsub [mem], 1 / jcc)或指针或扩展精度算术相比不是很好(手动执行带有比较和随身携带的进位)即使在32位模式下,另一个paddq也很糟糕,其中64位整数寄存器不可用。如果您在加载/存储uops上没有瓶颈,通常最好将溢出/重新加载到内存而不是XMM寄存器。

如果还需要从循环外部调用函数(清理或其他东西),请编写包装器或使用add $-128, %rsp ; call ; sub $-128, %rsp来保留这些版本中的红区。 (请注意,-128可编码为imm8+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之前对齐call)。这根本不是面向未来的,但应该会发挥作用。

GNU C有一个__attribute__((target("options"))) x86 function attribute,但它无法用于任意选项,而-mno-redzone不是您可以在每个选项上切换的其中一个函数基础,或编译单元中的#pragma GCC target ("options")

您可以使用

之类的内容
__attribute__(( target("sse4.1,arch=core2") ))
void penryn_version(void) {
  ...
}

但不是__attribute__(( target("-mno-redzone") ))

有一个#pragma GCC optimize和一个optimize函数属性(两者都不适用于生产代码),但#pragma GCC optimize ("-mno-redzone")无论如何都无法正常工作。我认为即使在调试版本中,也可以使用-O2来优化一些重要的函数。您可以设置-f个选项或-O

答案 3 :(得分:0)

不确定但是查看GCC documentation for function attributes,我找到了可能感兴趣的stdcall函数属性。

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

答案 4 :(得分:0)

如何创建一个用C语言编写的虚函数,除了调用内联汇编外什么都不做?