Подтвердить что ты не робот

Встроенная сборка, которая захватывает красную зону

Я пишу криптографическую программу, а ядро ​​(широкая процедура умножения) записывается в сборку x86-64, как для скорости, так и потому, что она широко использует команды типа adc, которые не легко доступны из C. я не хотите встраивать эту функцию, потому что она большая и она несколько раз вызывалась во внутреннем цикле.

В идеале я также хотел бы определить пользовательское соглашение о вызове для этой функции, потому что внутри он использует все регистры (кроме rsp), не сжимает его аргументы и не возвращается в регистры. Прямо сейчас он адаптировался к соглашению о вызове C, но, конечно, это делает его медленнее (примерно на 10%).

Чтобы избежать этого, я могу вызвать его с помощью asm("call %Pn" : ... : my_function... : "cc", all the registers);, но есть ли способ сказать GCC, что команда вызова беспорядочна со стеком? В противном случае GCC просто поместит все эти регистры в красную зону, а верхняя часть будет сбита. Я могу скомпилировать весь модуль с -mno-red-zone, но я бы предпочел, чтобы сказать GCC, что, например, верхние 8 байт красной зоны будут сбиты, так что ничего там не будет.

4b9b3361

Ответ 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.