Встроенная сборка, перекрывающая красную зону - PullRequest
20 голосов
/ 17 июня 2011

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

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

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

Ответы [ 5 ]

5 голосов
/ 18 июня 2011

Из вашего первоначального вопроса я не осознавал, что 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 голосов
/ 18 июня 2011

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

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

1 голос
/ 21 ноября 2017

Максимальным быстродействием может быть запись всего внутреннего цикла в 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.

0 голосов
/ 02 июля 2013

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

0 голосов
/ 18 июня 2011

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...