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
велики, и по какой-то причине они не могут встроиться в своих вызывающих.