Встроенная сборка, которая захватывает красную зону
Я пишу криптографическую программу, а ядро (широкая процедура умножения) записывается в сборку x86-64, как для скорости, так и потому, что она широко использует команды типа adc
, которые не легко доступны из C. я не хотите встраивать эту функцию, потому что она большая и она несколько раз вызывалась во внутреннем цикле.
В идеале я также хотел бы определить пользовательское соглашение о вызове для этой функции, потому что внутри он использует все регистры (кроме rsp
), не сжимает его аргументы и не возвращается в регистры. Прямо сейчас он адаптировался к соглашению о вызове C, но, конечно, это делает его медленнее (примерно на 10%).
Чтобы избежать этого, я могу вызвать его с помощью asm("call %Pn" : ... : my_function... : "cc", all the registers);
, но есть ли способ сказать GCC, что команда вызова беспорядочна со стеком? В противном случае GCC просто поместит все эти регистры в красную зону, а верхняя часть будет сбита. Я могу скомпилировать весь модуль с -mno-red-zone, но я бы предпочел, чтобы сказать GCC, что, например, верхние 8 байт красной зоны будут сбиты, так что ничего там не будет.
Ответы
Ответ 1
Из вашего первоначального вопроса я не понимал, что 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
Ответ 2
Не можете ли вы просто изменить свою функцию сборки для соответствия требованиям сигнала в ABI x86-64, сдвинув указатель стека на 128 байт при входе в вашу функцию?
Или, если вы обращаетесь к самому указателю возврата, поместите сдвиг в макрос вашего вызова (так sub %rsp; call...
)
Ответ 3
Не уверен, но глядя на документацию GCC для атрибутов функций, я нашел атрибут функции stdcall
, который может представлять интерес.
Мне все еще интересно, что вы считаете проблемным с вашей версией вызова asm. Если это просто эстетика, вы можете превратить ее в макрос или встроенную функцию.
Ответ 4
Как насчет создания фиктивной функции, написанной на C, и ничего не делает, кроме вызова встроенной сборки?
Ответ 5
Максимально возможный способ состоит в том, чтобы написать весь внутренний цикл в asm (включая инструкции call
, если он действительно стоит его развернуть, но не встроенный). Конечно, правдоподобно, если полная вставка вызывает слишком много промахов uop-cache в другом месте).
В любом случае, C вызовите функцию asm, содержащую ваш оптимизированный цикл.
Кстати, clobbering всех регистров затрудняет gcc, чтобы создать очень хороший цикл, так что вы вполне можете выйти из оптимизма всего цикла. (например, может содержать указатель в регистре и конечный указатель в памяти, поскольку cmp mem,reg
по-прежнему достаточно эффективен).
Посмотрите на код gcc/clang wrap вокруг оператора 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 с задержкой 1 c. (Он по-прежнему 1 мкп на AMD, но более высокая задержка, особенно на семействе Bulldozer). Выполнение скалярного целочисленного материала в регистрах XMM не намного хуже и может стоить того, если общая пропускная способность вашего устройства является вашим узким местом, или это экономит больше разливов/перезарядов, чем это стоит.
Но, конечно, XMM не очень жизнеспособен для счетчиков циклов (paddd
/pcmpeq
/pmovmskb
/cmp
/jcc
или psubd
/ptest
/jcc
не очень велики до sub [mem], 1
/jcc), или для указателей, или для арифметики с расширенной точностью (ручное выполнение с сопоставлением и переносом с другим paddq
засасывает даже в 32-битном режиме, где 64-битные целочисленные регистры 't доступно). Обычно лучше разливать/перезагружать в память вместо 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
.