Почему llvm и gcc используют разные прологи функций на x86 64?

Тривиальная функция, которую я компилирую с помощью gcc и clang:

void test() {
    printf("hm");
    printf("hum");
}


$ gcc test.c -fomit-frame-pointer -masm=intel -O3 -S

sub rsp, 8
.cfi_def_cfa_offset 16
mov esi, OFFSET FLAT:.LC0
mov edi, 1
xor eax, eax
call    __printf_chk
mov esi, OFFSET FLAT:.LC1
mov edi, 1
xor eax, eax
add rsp, 8
.cfi_def_cfa_offset 8
jmp __printf_chk

И

$ clang test.c -mllvm --x86-asm-syntax=intel -fomit-frame-pointer -O3 -S    

# BB#0:
push    rax
.Ltmp1:
.cfi_def_cfa_offset 16
mov edi, .L.str
xor eax, eax
call    printf
mov edi, .L.str1
xor eax, eax
pop rdx
jmp printf                  # TAILCALL

Разница, которая меня интересует, заключается в том, что gcc использует sub rsp, 8/add rsp, 8 для пролога функции, а clang использует push rax/pop rdx.

Почему компиляторы используют разные прологи функций? Какой вариант лучше? push и pop, безусловно, кодируют более короткие инструкции, но они быстрее или медленнее, чем add и sub?

Причина возни со стеком в первую очередь, по-видимому, заключается в том, что abi требует, чтобы rsp был выровнен по 16 байтам для неконечных процедур. Я не смог найти какие-либо флаги компилятора, которые их удаляют.

Судя по вашим ответам, кажется, что push & pop лучше. push rax + pop rdx = 1 + 1 = 2 против sub rsp, 8 + add rsp, 8 = 4 + 4 = 8. Таким образом, первая пара экономит 6 байт без каких-либо затрат.


person Björn Lindqvist    schedule 21.07.2015    source источник
comment
Это вопрос выбора. Трудно сказать, какой вариант лучше. Вероятно, оба варианта довольно похожи с точки зрения производительности.   -  person Jabberwocky    schedule 21.07.2015
comment
Re: ваше редактирование. Да, ABI гарантирует, что при входе в функцию (%rsp + 8) выровнено по 16B. (Я отредактировал большую часть этого комментария в своем ответе).   -  person Peter Cordes    schedule 21.07.2015


Ответы (2)


В Intel sub / add вызовет механизм стека для вставки дополнительной uop для синхронизации %rsp для части выполнения конвейера вне порядка. (См. документацию Agner Fog по микроархиву, в частности, стр. 91, о механизме стека. Насколько мне известно, он по-прежнему работает на Haswell, как и на Pentium M, когда ему нужно вставить лишние мопы.

push / pop потребует меньше операций с объединенным доменом и поэтому, вероятно, будет более эффективным, даже если они используют порты хранения/загрузки. Они появляются между парами call/ret.

Итак, push/pop по крайней мере не будет медленнее, но займет меньше байтов инструкций. Лучшая плотность I-кэша — это хорошо.

Кстати, я думаю, что смысл пары insns заключается в том, чтобы стек был выровнен по 16B после того, как call подтолкнет адрес возврата 8B. Это тот случай, когда ABI требует полубесполезных инструкций. Более сложные функции, которым требуется место в стеке для сброса локальных переменных, а затем перезагружать их после вызова функции, обычно будут sub $something, %rsp резервировать место.

SystemV (Linux) amd64 ABI гарантирует, что при входе в функцию (%rsp + 8), где будут находиться аргументы в стеке, если таковые имеются, будут выровнены по 16 байтам. (http://x86-64.org/documentation/abi.pdf). Вы должны сделать так, чтобы это имело место для любой функции, которую вы вызываете, или это ваша вина, если они отказываются от использования загрузки, выровненной по SSE. Или иначе произойдет сбой из-за предположений о том, как они могут использовать AND для маскировки адреса или чего-то еще.

person Peter Cordes    schedule 21.07.2015
comment
Да, это просто для поддержания выравнивания стека. - person WhatsUp; 21.07.2015
comment
Также обратите внимание, что в большинстве случаев функции времени выделяют некоторое пространство для локальных переменных, и вариант sub в этом случае более эффективен. Предположительно, авторы компилятора просто не оптимизировали для случая, когда локальные не нужны. - person Jester; 21.07.2015
comment
Да, нелистовые функции с очень небольшим количеством локальных переменных — редкий случай. Я думаю, что использование clang push/pop данных, которые ему не нужны, является аккуратной оптимизацией. - person Peter Cordes; 21.07.2015

Согласно экспериментам, которые я провел на своей машине, push/pop имеют ту же скорость, что и add/sub. Думаю, так должно быть на всех современных компьютерах.

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

person WhatsUp    schedule 21.07.2015
comment
Какой эксперимент? Вы тестировали что-то, что было узким местом в пропускной способности uop? Я согласен, что, вероятно, большую часть времени разницы нет. - person Peter Cordes; 21.07.2015
comment
Я сделал самую наивную вещь: несколько тысяч раз скопировал инструкцию (на самом деле с помощью макросов), зациклил всю и запустил. Я не уверен, является ли это узким местом на uop. Не могли бы вы подтвердить? - person WhatsUp; 21.07.2015
comment
add с одними и теми же регистрами каждый раз нуждается в выводе предыдущего в качестве ввода, что делает задержку ограничителем. add имеет пропускную способность 3 за цикл в SnB/IvB и 4 за цикл в Haswell, если они независимы. push может выдержать 1/цикл, pop 2/цикл. Как всегда с современными процессорами, важен контекст (какие другие insns конкурируют за ресурсы выполнения и как они вписываются в цепочку зависимостей). - person Peter Cordes; 21.07.2015