我正在编写一个加密程序,核心(一个宽乘法例程)是用x86-64汇编编写的,这既是为了速度,也是因为它广泛使用了adc
等指令,而这些指令从C中很难访问。我不想内联这个函数,因为它很大,而且在内部循环中被调用了好几次。
理想情况下,我还想为这个函数定义一个自定义的调用约定,因为它在内部使用所有寄存器(rsp
除外),不破坏其参数,并在寄存器中返回。现在,它已经适应了C调用约定,但这当然会使它变慢(大约10%)。
为了避免这种情况,我可以用asm("call %Pn" : ... : my_function... : "cc", all the registers);
调用它,但有没有办法告诉GCC调用指令会干扰堆栈?否则,GCC将把所有这些寄存器都放在红色区域,而顶部的寄存器将被打得落花流水。我可以用-mno-red-zone编译整个模块,但我更喜欢告诉GCC,比如说,红色区域的前8个字节将被删除,这样它就不会在那里放任何东西。
根据您最初的问题,我没有意识到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
最大性能的方法可能是在asm中写入整个内部循环(包括call
指令,如果真的值得展开但不内联的话。如果完全内联导致其他地方的uop缓存丢失过多,这当然是合理的)。
无论如何,让C调用一个包含优化循环的asm函数。
顺便说一句,敲打所有寄存器会使gcc很难做出一个非常好的循环,所以你很可能会提前优化整个循环。(例如,可能将指针保存在寄存器中,将结束指针保存在内存中,因为cmp mem,reg
仍然相当有效)。
看看围绕asm
语句的代码gcc/clang包装,该语句修改数组元素(在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而不是内存目标add [mem], -1
/jnz
。
如果你自己用asm编写整个循环,而不是把热循环的那部分留给编译器,你可能会做得更好
如果可能的话,可以考虑使用一些XMM寄存器进行整数运算,以减少整数寄存器上的寄存器压力。在英特尔CPU上,在GP和XMM寄存器之间移动只需要1个ALU-uop,延迟为1c。(AMD仍然是1 uop,但延迟更高,尤其是推土机系列)。在XMM寄存器中执行标量整数操作并不差多少,如果总uop吞吐量是您的瓶颈,或者它可以节省比成本更多的溢出/重新加载,那么这可能是值得的。
当然,XMM对于循环计数器(paddd
/pcmpeq
/pmovmskb
/cmp
/jcc
或psubd
/ptest
/jcc
与sub [mem], 1
/jcc相比不太好)、指针或扩展精度算术(手动执行比较并与另一个paddq
进行进位,即使在64位整数regs不可用的32位模式下也很糟糕)来说是不可行的。如果您在加载/存储uops方面没有遇到瓶颈,那么通常最好将溢出/重新加载到内存,而不是XMM寄存器。
如果您还需要从循环外调用函数(cleanup或其他什么),请编写一个包装器或使用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-red- zone
不是可以在每个函数的基础上切换的,也不是可以在编译单元中使用#pragma GCC target ("options")
切换的。
你可以使用之类的东西
__attribute__(( target("sse4.1,arch=core2") ))
void penryn_version(void) {
...
}
而不是CCD_ 35。
有一个#pragma GCC optimize
和optimize
函数属性(这两个属性都不适用于生产代码),但#pragma GCC optimize ("-mno-red-zone")
也不起作用。我认为这个想法是让-O2
优化一些重要的功能,即使在调试构建中也是如此。您可以设置-f
选项或-O
。
不过,您可以将函数本身放在一个文件中,并使用-mno-red-zone
编译该编译单元。(希望LTO不会破坏任何东西…)
难道不能通过在进入函数时将堆栈指针移动128字节来修改汇编函数以满足x86-64 ABI中的信号要求吗?
或者,如果您引用的是返回指针本身,请将移位放入调用宏(所以sub %rsp; call...
)
不确定,但在查看GCC文档中的函数属性时,我发现了可能感兴趣的stdcall
函数属性。
我仍然想知道你发现你的asm调用版本有什么问题。如果只是美学,您可以将其转换为宏或内联函数。
创建一个用C编写的伪函数,只调用内联程序集,怎么样?