最大性能的方法可能是在 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 将一个单独的计数器向下计数至零。但它使用加载/添加-1/存储而不是内存目标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/jcc 相比不是很好到sub [mem], 1 / jcc),或指针,或扩展精度算术(手动进行进位比较和进位另一个paddq即使在64位整数regs的32位模式下也很糟糕'不可用)。如果您在加载/存储微指令上没有遇到瓶颈,通常最好将溢出/重新加载到内存而不是 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,这将使他们保留足够的堆栈空间,因为无论如何他们都必须调整堆栈(在 call 之前对齐 rsp)。这根本不是面向未来的,但应该会起作用。
GNU C 有一个__attribute__((target("options"))) x86 function attribute,但它不能用于任意选项,并且-mno-red- zone 不是您可以基于每个功能或使用@987654350 切换的选项之一@ 在编译单元中。
你可以使用类似的东西
__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 不会破坏任何东西……)