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

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

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

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

5 ответов

Исходя из вашего первоначального вопроса, я не понял, что gcc ограничил использование красной зоны для конечных функций. Я не думаю, что этого требует ABI x86_64, но это разумное упрощающее допущение для компилятора. В этом случае вам нужно только сделать функцию, вызывающую подпрограмму сборки, не листовой для целей компиляции:

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

Способ максимальной производительности мог бы написать весь внутренний цикл в asm (включая call инструкции, если действительно стоит развернуть, но не в строке. Конечно, правдоподобно, если полное встраивание вызывает слишком много пропусков uop-кэша в другом месте).

В любом случае, пусть C вызывает функцию asm, содержащую ваш оптимизированный цикл.

Кстати, засорение всех регистров мешает 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 с задержкой 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,

Разве вы не можете просто изменить свою функцию сборки для соответствия требованиям сигнала в ABI x86-64, смещая указатель стека на 128 байтов при входе в вашу функцию?

Или, если вы ссылаетесь на сам указатель возврата, поместите сдвиг в макрос вызова (так sub %rsp; call...)

Не уверен, но, глядя на документацию GCC для атрибутов функции, я нашел stdcall Атрибут функции, который может представлять интерес.

Мне все еще интересно, что вы считаете проблематичным с вашей версией asm call. Если это просто эстетика, вы можете превратить его в макрос или встроенную функцию.

Как насчет создания фиктивной функции, которая написана на C и ничего не делает, кроме как вызывает встроенную сборку?

Другие вопросы по тегам