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