我正在编写一个加密程序,并且核心(一个广泛的乘法例程)是用x86-64汇编编写的,这两者都是为了提高速度,因为它广泛使用了不容易从C中访问的adc
之类的指令我不想内联这个函数,因为它很大,并且在内循环中被调用了几次。
理想情况下,我还想为此函数定义一个自定义调用约定,因为在内部它使用所有寄存器(rsp
除外),不会破坏其参数,并返回寄存器。现在,它适应了C调用约定,但当然这会使它变慢(大约10%)。
为了避免这种情况,我可以用asm("call %Pn" : ... : my_function... : "cc", all the registers);
来调用它,但有没有办法告诉GCC调用指令与堆栈混淆?否则GCC会将所有这些寄存器放在红色区域中,而顶部的寄存器将被破坏。我可以使用-mno-red-zone编译整个模块,但是我更喜欢告诉GCC,比方说,红色区域的前8个字节将被破坏,以便它不会放任何东西。 / p>
答案 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
/ jcc
或psubd
/ ptest
/ jcc
与sub [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语言编写的虚函数,除了调用内联汇编外什么都不做?