# #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
.)