Почему в эпилоге clang используется «добавить $N, %rsp «вместо» mov %rbp, %rsp` для восстановления «%rsp»?

# #assembly #clang #x86-64 #micro-optimization

Вопрос:

Учесть следующее:

 ammarfaizi2@integral:/tmp$ vi test.c
ammarfaizi2@integral:/tmp$ cat test.c

extern void use_buffer(void *buf);

void a_func(void)
{
    char buffer[4096];
    use_buffer(buffer);
}

__asm__("emit_mov_rbp_to_rsp:ntmovq %rbp, %rsp");

ammarfaizi2@integral:/tmp$ clang -Wall -Wextra -c -O3 -fno-omit-frame-pointer test.c -o test.o
ammarfaizi2@integral:/tmp$ objdump -d test.o

test.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <emit_mov_rbp_to_rsp>:
   0: 48 89 ec              mov    %rbp,%rsp
   3: 66 2e 0f 1f 84 00 00  cs nopw 0x0(%rax,%rax,1)
   a: 00 00 00 
   d: 0f 1f 00              nopl   (%rax)

0000000000000010 <a_func>:
  10: 55                    push   %rbp
  11: 48 89 e5              mov    %rsp,%rbp
  14: 48 81 ec 00 10 00 00  sub    $0x1000,%rsp
  1b: 48 8d bd 00 f0 ff ff  lea    -0x1000(%rbp),%rdi
  22: e8 00 00 00 00        call   27 <a_func 0x17>
  27: 48 81 c4 00 10 00 00  add    $0x1000,%rsp
  2e: 5d                    pop    %rbp
  2f: c3                    ret    
ammarfaizi2@integral:/tmp$ 
 

В конце a_func() , перед возвращением, восстанавливается эпилог функции %rsp . Он использует add $0x1000, %rsp то, что дает 48 81 c4 00 10 00 00 .

Разве он не может просто использовать mov %rbp, %rsp то, что дает только 3 байта 48 89 ec ?

Почему clang не использует более короткий путь ( mov %rbp, %rsp )?

С учетом компромисса по размеру кода, в чем преимущество использования add $0x1000, %rsp вместо mov %rbp, %rsp ?

Обновление (дополнительно)

Даже при -Os этом это все равно приводит к одному и тому же коду. Поэтому я думаю, что должна быть рациональная причина, чтобы этого избежать mov %rbp, %rsp .

 ammarfaizi2@integral:/tmp$ clang -Wall -Wextra -c -Os -fno-omit-frame-pointer test.c -o test.o
ammarfaizi2@integral:/tmp$ objdump -d test.o

test.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <emit_mov_rbp_to_rsp>:
   0:   48 89 ec                mov    %rbp,%rsp

0000000000000003 <a_func>:
   3:   55                      push   %rbp
   4:   48 89 e5                mov    %rsp,%rbp
   7:   48 81 ec 00 10 00 00    sub    $0x1000,%rsp
   e:   48 8d bd 00 f0 ff ff    lea    -0x1000(%rbp),%rdi
  15:   e8 00 00 00 00          call   1a <a_func 0x17>
  1a:   48 81 c4 00 10 00 00    add    $0x1000,%rsp
  21:   5d                      pop    %rbp
  22:   c3                      ret    
ammarfaizi2@integral:/tmp$ 
 

Ответ №1:

Если бы он вообще использовал RBP в качестве указателя кадра, да, mov %rbp, %rsp он был бы более компактным и, по крайней мере, таким же быстрым на всех микроархитектурах x86. (устранение mov, вероятно, даже работает над этим). Тем более, когда константа добавления не вписывается в imm8.

Это, вероятно, пропущенная оптимизация, очень похожая на https://bugs.llvm.org/show_bug.cgi?id=10319 (который предлагает использовать leave вместо mov/pop, что обойдется в 1 дополнительный uop на Intel, но сэкономит еще 3 байта). Это указывает на то, что общая экономия статического размера кода в обычных случаях довольно мала, но не учитывает преимущества эффективности. В обычных сборках ( -O2 без -fno-omit-frame-pointer ) только несколько функций вообще будут использовать указатель кадра (только при использовании VLA / alloca или чрезмерном выравнивании стека), поэтому возможная выгода еще меньше.

Из этой ошибки кажется, что это просто глазок, который LLVM не утруждает себя поиском, потому что многим функциям также необходимо восстановить другие регистры, поэтому вам на самом деле нужно add какое-то другое значение, чтобы указать RSP ниже других нажатий.

(GCC иногда использует mov для восстановления правил, сохраненных при вызове, чтобы их можно было использовать leave . С указателем кадра это делает режим адресации довольно компактным для кодирования, хотя 4-байтовое qword mov -8(%rbp), %r12 , конечно, все еще не так мало, как 2-байтовое pop. И если у нас нет указателя на кадр (например, в -O2 коде), mov %rbp, %rsp это никогда не было вариантом.)


Прежде чем рассмотреть причину «не стоит искать», я подумал еще об одном незначительном преимуществе:

После вызова функции, которая сохраняет/восстанавливает RBP, RBP является результатом загрузки. Поэтому после mov %rbp, %rsp дальнейшего использования RSP потребуется дождаться этой загрузки. Возможно, некоторые угловые случаи заканчиваются узким местом из-за задержки в хранении, по сравнению с изменением регистра всего за 1 цикл.

Но в целом это вряд ли стоит дополнительного размера кода; я ожидаю, что такие угловые случаи редки. Хотя это новое значение RSP необходимо для a pop %rbp , поэтому восстановленное значение RBP вызывающего абонента является результатом цепочки из двух загрузок после нашего возвращения. (К счастью ret , есть предсказание ветвления, чтобы скрыть задержку.)

Поэтому, возможно, стоит попробовать оба способа в некоторых тестах; например, сравнить это с измененной версией LLVM на некоторых стандартных тестах, таких как SPECint.

Комментарии:

1. Спасибо, кажется, у нас есть дубликат этого bugs.llvm.org/show_bug.cgi?id=10319

2. Я бы ожидал, что ответ будет таким: «потому что тогда вы можете использовать RBP в качестве дополнительного регистра». (Очевидно, что компиляция должна выдавать смещения из RSP, а не из RBP, но это технически просто).

3. @IraBaxter: Это скомпилировано с использованием clang -Os -fno-omit-frame-pointer . Люди иногда этого хотят (и IIRC-это -Os GCC по умолчанию), поэтому, если вы уже заставляете компилятор тратить RBP на то, чтобы быть указателем фрейма, вы хотите извлечь из этого максимальную выгоду. Но да, раз уж вы упомянули об этом, изменил формулировку, чтобы напомнить читателям, что большинство программ построено без -fno-omit-frame-pointer .)