(Прежде всего, не ожидайте эффективных решений на -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 дополнительного стекаместо для переменных, но в основном так же, как требуется для выравнивания, а не для дополнительных.