GCC помещает регистровые аргументы в стек с пробелом ниже локальных переменных? - PullRequest
2 голосов
/ 30 октября 2019

Я попытался посмотреть на ассемблерный код для очень простой программы.

int func(int x) {
    int z = 1337;
    return z;
} 

С GCC -O0 каждая переменная C имеет адрес памяти, который не оптимизирован, поэтому gcc выдает свой регистр arg:( Godbolt, gcc5.5 -O0 -fverbose-asm )

func:
        pushq   %rbp  #
        movq    %rsp, %rbp      #,
        movl    %edi, -20(%rbp) # x, x
        movl    $1337, -4(%rbp) #, z
        movl    -4(%rbp), %eax  # z, D.2332
        popq    %rbp    #
        ret

По какой причине параметр функции x помещается в стек ниже локальных переменных? Почему бы не поместить его в -4(%rbp), а локальный ниже этого?

А при размещении его ниже локальных переменных, почему бы не разместить его в -8(%rbp)?

Зачем оставлять пробел,использовать больше , чем необходимо? Разве это не может коснуться новой строки кэша, которая иначе не была бы затронута в этой функции листьев?

1 Ответ

3 голосов
/ 31 октября 2019

(Прежде всего, не ожидайте эффективных решений на -O0. Оказывается, что то, что вы заметили на -O0, все еще происходит на -O3, если мы используем volatile или другие вещи для принудительной компиляциивыделить место в стеке, иначе этот вопрос будет гораздо менее интересным.)

По какой причине параметр функции x помещается в стек ниже локальных переменных?

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

Но зачем сохранять его в стеке позже, чем это действительно необходимо?

Из-за известных ошибок GCC, приводящих к пропущенной оптимизации, приводящих к потере пространства в стеке. (мне кажется, я видел отчет о том, что иногда используют дополнительные 16 байтов пространства, когда GCC нужно переместить RSP (в отличие от того, где он просто использует красную зону), но не могу найти его в bugzilla GCC). )

Обратите внимание, что x86-64 System V ABI предписывает 16-байтовое выравнивание стека перед call. После push %rbp и установки RBP в качестве указателя кадра RBP и RSP выравниваются по 16 байтов. -20(%rbp) находится в том же выровненном 16-байтовом фрагменте стекового пространства, что и -8(%rbp), поэтому этот пробел не рискует прикоснуться к новой строке кэша или странице, к которой мы бы еще не прикоснулись. (Естественно выровненный кусок памяти не может пересекать любую границу, более широкую, чем он сам, и строки кэша x86-64 всегда имеют длину не менее 32 байтов; в наши дни всегда 64 байта.)

Однако это становится пропущенной оптимизацией, если мы добавим 2-й аргумент, int y: gcc5.5 (и текущий gcc9.2 -O0) выльется в -24(%rbp), что может быть в новой строке кэша.


Оказывается, эта пропущенная оптимизация , а не только потому, что вы использовали -O0 (компилируйте быстро, пропустите большинство проходов оптимизации, сделайте плохой asm ). Поиск пропущенных оптимизаций в выводе -O0 не имеет смысла, если только они не присутствуют на уровне оптимизации, который кого-то волнует, особенно -Os, -O2 или -O3.

Мы можем это доказатьс кодом, который использует volatile, чтобы по-прежнему заставлять gcc выделять место в стеке для аргументов / locals на -O3 Другой вариант - передать их адрес другой функции, но тогда GCC придется зарезервировать пространство вместо простого использованиякрасная зона под RSP.

int *volatile sink;

int func(int x, int y) {
    sink = &x;
    sink = &y;
    int z = 1337;
    sink = &z;
    return z;
}

( Godbolt , gcc9.2 )

gcc9.2 -O3  (hand-edited comments)
func(int, int):
        leaq    -20(%rsp), %rax                 # &x
        movq    %rax, sink(%rip)        # tmp84, sink
        leaq    -24(%rsp), %rax                 # &y
        movq    %rax, sink(%rip)        # tmp86, sink
        leaq    -4(%rsp), %rax                  # &z
        movq    %rax, sink(%rip)        # tmp88, sink
        movl    $1337, %eax     #,
        ret     
sink:
        .zero   8

Интересный факт: clang -O3выдает аргументы стека перед сохранением их адреса в sink, как будто это был std::atomic релиз-хранилище адреса, и другой поток может загрузить их значение после получения указателя из sink. Но это не делает это для z. Это просто пропущенная оптимизация, чтобы фактически пролить x и y, и я могу только догадываться о том, какая часть внутреннего механизма clang может быть виновата.

В любом случае, clang выделяет z в -4(%rsp), x при -8, y при -12. Так что по какой-то причине clang также решает поместить слоты для разлива арг ниже местных жителей.


Связанный:

  • Трата в распределении памяти для локальных переменных обсуждает GCC main, не предполагающий 16-байтовое выравнивание при входе в main.

  • несколько возможных дубликатов о выделении GCC дополнительного стекаместо для переменных, но в основном так же, как требуется для выравнивания, а не для дополнительных.

...