内联assembly破坏了红色区域

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

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

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

从你原来的问题我没有意识到gcc限制红区使用到叶子function。 我不认为这是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叶子的代码放回到subq $160, %rsp

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

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

最大性能方式可能是在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吞吐量是你的瓶颈,或者它节省了比它更多的溢出/重新加载,那么它可能是值得的。

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


如果你还需要从循环外部调用函数(清理或其他东西),写一个包装器或使用add $-128, %rsp ; call ; sub $-128, %rsp add $-128, %rsp ; call ; sub $-128, %rsp 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,这将使它们保留足够的堆栈空间,因为它们无论如何都必须调整堆栈(在call之前对齐rsp )。 这根本不是面向未来的,但应该会发生。


GNU C有一个__attribute__((target("options"))) x86函数属性 ,但它不能用于任意选项-mno-redzone不是你可以在每个函数的基础上切换的那个,或者是#pragma GCC target ("options")编译单元中的#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优化一些重要的function。 您可以设置-f选项或-O

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

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

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