最大性能的方法可能是在 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 寄存器。pcmpeq
pmovmskb
cmp
jcc
psubd
ptest
jcc
sub [mem], 1
paddq
如果您还需要从循环外部调用该函数(清理或其他),请编写一个包装器或用于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 不会破坏任何东西……)