Рассмотрим этот код C:
void foo(void);
long bar(long x) {
foo();
return x;
}
Когда я компилирую его в GCC 9.3 с помощью -O3
или -Os
, я получаю это:
bar:
push r12
mov r12, rdi
call foo
mov rax, r12
pop r12
ret
Выходные данные из clang идентичны, за исключением того, что они выбраны rbx
вместо r12
сохраненного в регистре.
Тем не менее, я хочу / ожидаю увидеть сборку, которая выглядит примерно так:
bar:
push rdi
call foo
pop rax
ret
На английском вот что происходит:
- Перенесите старое значение сохраненного в регистр пользователя в стек
- Переместиться
x
в этот сохраненный регистр - Вызов
foo
- Переместиться
x
из сохраненного в регистре вызываемого абонента в регистр возвращаемого значения - Вставьте стек, чтобы восстановить старое значение сохраненного в регистре вызываемого абонента.
Зачем вообще возиться с регистром, сохраненным вызываемым абонентом? Почему бы не сделать это вместо этого? Это кажется короче, проще и, вероятно, быстрее:
- Толкать
x
в стек - Вызов
foo
- Выскочить
x
из стека в регистр возвращаемого значения
Моя сборка неправильная? Это как-то менее эффективно, чем возиться с дополнительным регистром? Если ответом на оба вопроса является «нет», то почему бы ни GCC, ни Clang не сделали это таким образом?
Изменить: Вот менее простой пример, чтобы показать, что это происходит, даже если переменная используется осмысленно:
long foo(long);
long bar(long x) {
return foo(x * x) - x;
}
Я получаю это:
bar:
push rbx
mov rbx, rdi
imul rdi, rdi
call foo
sub rax, rbx
pop rbx
ret
Я бы предпочел это:
bar:
push rdi
imul rdi, rdi
call foo
pop rdi
sub rax, rdi
ret
На этот раз это только одна инструкция против двух, но основная концепция та же.
Ответы:
TL: DR:
foo
не происходит сохранение / восстановление RBX.Компиляторы являются сложными частями машин. Они не «умны», как человек, и дорогие алгоритмы для поиска всевозможных оптимизаций часто не стоят затрат в дополнительное время компиляции.
Я сообщил об этом как об ошибке GCC 69986 - меньший код возможен с -Os с помощью push / pop для разлива / перезагрузки в 2016 году ; не было никакой активности или ответов от разработчиков GCC. : /
Немного связано: ошибка GCC 70408 - повторное использование одного и того же регистра, сохраненного при вызове, в некоторых случаях дало бы меньший код - разработчики компилятора сказали мне, что GCC потребуется огромная работа, чтобы выполнить эту оптимизацию, потому что она требует выбора порядка оценки из двух
foo(int)
вызовов на основе того, что сделало бы целевой асс проще.Если
foo
не сохранить / восстановитьrbx
себя, существует компромисс между пропускной способностью (счетчиком команд) и дополнительной задержкой сохранения / перезагрузки вx
цепочке зависимостей -> retval.Компиляторы обычно предпочитают задержку перед пропускной способностью, например, используя 2x LEA вместо
imul reg, reg, 10
(задержка 3 цикла, пропускная способность 1 / тактовая частота), потому что большинство кодов в среднем значительно меньше, чем 4 моп / такт на типичных конвейерах с шириной 4, таких как Skylake. (Больше инструкций / мопов занимают больше места в ROB, уменьшая, насколько далеко вперед может видеть то же самое окно с неупорядоченным порядком, и выполнение фактически взрывное с остановками, вероятно, составляющими некоторые из менее чем 4 мопов / часы в среднем.)Если сделать
foo
RBX push / pop, то выиграть немного времени не получится. Восстановление, выполняемое непосредственно перед заменой, аret
не сразу после, вероятно, не имеет значения, если только неret
произойдет неправильный прогноз или промах I-кэша, который задерживает выборку кода по адресу возврата.Большинство нетривиальных функций сохраняют / восстанавливают RBX, поэтому часто не стоит полагать, что если оставить переменную в RBX, это на самом деле означает, что она действительно остается в регистре на протяжении всего вызова. (Хотя рандомизация выбора регистров, сохраняемых вызовом, может быть хорошей идеей для смягчения этого иногда.)
Так что да
push rdi
/pop rax
было бы более эффективно в этом случае, и это, вероятно, пропущенная оптимизация для крошечных неконечных функций, в зависимости от того, чтоfoo
делает и баланс между дополнительной задержкой сохранения / перезагрузкиx
и большим количеством инструкций для сохранения / восстановления вызывающей стороныrbx
.Метаданные для разматывания стека могут представлять здесь изменения RSP, как если бы они использовались
sub rsp, 8
для разлива / перезагрузкиx
в слот стека. (Но составители не знают эту оптимизацию либо, использоватьpush
для резервного пространства и инициализировать переменную. Что C / C ++ компилятор может использовать инструкции толчок популярности для создания локальных переменных, а не просто увеличение особ раз? . И делать это для более один локальный var приведет к увеличению.eh_frame
метаданных стека, потому что вы перемещаете указатель стека отдельно при каждом нажатии. Это не мешает компиляторам использовать push / pop для сохранения / восстановления сохраненных вызовом регистров.)IDK, если бы стоило учить компиляторов искать эту оптимизацию
Возможно, это хорошая идея для всей функции, а не для одного вызова внутри функции. И, как я уже сказал, это основано на пессимистическом предположении, что
foo
все равно сохранит / восстановит RBX. (Или оптимизация пропускной способности, если вы знаете, что задержка от x до возвращаемого значения не важна. Но компиляторы этого не знают и обычно оптимизируют для задержки).Если вы начнете делать это пессимистическое предположение в большом количестве кода (например, вокруг отдельных вызовов функций внутри функций), вы начнете получать больше случаев, когда RBX не сохраняется / восстанавливается, и вы могли бы воспользоваться.
Вы также не хотите этого дополнительного сохранения / восстановления push / pop в цикле, просто сохраняйте / восстанавливайте RBX вне цикла и используйте сохраняемые вызовы регистры в циклах, которые выполняют вызовы функций. Даже без циклов, в общем случае большинство функций выполняет несколько вызовов функций. Эта идея оптимизации может быть применима, если вы действительно не используете
x
между любыми вызовами, непосредственно перед первым и после последнего, в противном случае у вас есть проблема поддержания выравнивания стека по 16 байтов для каждого,call
если вы делаете один щелчок после звоните, перед другим звонком.Компиляторы не очень хороши в крошечных функциях вообще. Но это не очень хорошо для процессоров. Вызовы не встроенных функций оказывают влияние на оптимизацию в лучшие времена, если только компиляторы не могут видеть внутренности вызываемого и делать больше предположений, чем обычно. Вызов не встроенной функции является неявным барьером памяти: вызывающий должен предположить, что функция может читать или записывать любые глобально доступные данные, поэтому все такие переменные должны быть синхронизированы с абстрактной машиной Си. (Анализ Escape позволяет сохранять локальные значения в регистрах между вызовами, если их адрес не экранирован от функции.) Кроме того, компилятор должен предположить, что регистры с вызовом-сглаживанием все засорены. Это отстой для плавающей запятой в x86-64 System V, которая не имеет сохраненных вызовов регистров XMM.
Крошечные функции, как
bar()
лучше, встраиваясь в своих абонентов. Скомпилируйте с-flto
таким образом, что в большинстве случаев это может происходить даже через границы файлов. (Указатели функций и границы разделяемой библиотеки могут победить это.)Я думаю, что одна из причин, по которой компиляторы не удосужились попытаться выполнить эти оптимизации, заключается в том, что для этого потребуется целая куча другого кода во внутренних компонентах компилятора , отличного от обычного стека и кода выделения регистров, который знает, как сохранить сохраняемый вызов регистрирует и использует их.
т. е. было бы много работы для реализации и много кода для поддержки, и если он слишком увлечен этим, он может сделать код хуже .
А также, что это (надеюсь) не имеет существенного значения; если это имеет значение, вы должны встраиваться
bar
в его вызывающего илиfoo
вbar
. Это хорошо, если только не существует много различныхbar
функций и ониfoo
велики, и по какой-то причине они не могут встроиться в своих вызывающих.источник