Максимальным быстродействием может быть запись всего внутреннего цикла в 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 вместо места назначения памяти add [mem], -1
/ jnz
.
Вероятно, вы можете добиться большего успеха, чем это, если вы напишете весь цикл самостоятельно в asm вместо того, чтобы оставить эту часть вашего горячего цикла компилятору.
Подумайте об использовании некоторых регистров XMM для целочисленной арифметики, чтобы уменьшить давление в регистрах на целочисленные регистры, если это возможно. На процессорах Intel перемещение между регистрами GP и XMM стоит всего 1 ALU uop с задержкой 1c. (Это все еще 1 моп для AMD, но большая задержка, особенно для семейства Bulldozer). Выполнение скалярных целочисленных операций в регистрах XMM не намного хуже, и может стоить того, если общая пропускная способность UOP является вашим узким местом, или это экономит больше разливов / повторных загрузок, чем стоит.
Но, конечно, XMM не очень подходит для счетчиков циклов (paddd
/ pcmpeq
/ pmovmskb
/ cmp
/ jcc
или psubd
/ ptest
/ jcc
невелики по сравнению в sub [mem], 1
/ jcc), или для указателей, или для арифметики с расширенной точностью (ручное выполнение с сравнением и переносом с другим paddq
отстой даже в 32-битном режиме, где 64-битные целочисленные регистры не требуются не доступно). Обычно лучше разливать / перезагружать в память вместо регистров 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 , но его нельзя использовать для произвольных опций , и -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
.