TL: DR:
- Внутренние компоненты компилятора, вероятно, не настроены так, чтобы легко искать эту оптимизацию, и, вероятно, она полезна только для небольших функций, а не внутри больших функций между вызовами.
- Инлайн для создания больших функций - лучшее решение в большинстве случаев
- Возможен компромисс между задержкой и пропускной способностью, если
foo
не сохранит / не восстановит RBX.
Компиляторы являются сложными частями машин. Они не «умны», как человек, и дорогие алгоритмы, позволяющие найти любую возможную оптимизацию, часто не стоят затрат в дополнительное время компиляции.
Я сообщил об этом как G CC ошибка 69986 - меньший код возможен с -O с помощью push / pop для разлива / перезагрузки в 2016 году ; не было активности или ответов от G CC devs. : /
Немного связано: G CC ошибка 70408 - повторное использование одного и того же регистра с сохранением вызовов в некоторых случаях даст меньший код - разработчики компилятора сказали мне, что потребуется огромное количество работайте для G CC, чтобы иметь возможность выполнить эту оптимизацию, потому что она требует выбора порядка оценки двух foo(int)
вызовов на основе того, что сделает целевой ассм проще.
Если foo
не сохраняет / восстанавливает rbx
, то существует компромисс между пропускной способностью (количеством команд) и дополнительной задержкой сохранения / перезагрузки в цепочке зависимостей x
-> retval.
Компиляторы обычно предпочитают задержку перед пропускной способностью, например, используя 2x LEA вместо imul reg, reg, 10
(задержка 3 цикла, пропускная способность 1 / тактовая частота), потому что большинство кодов в среднем значительно меньше 4 моп / такт на типичной 4-полосной трубопроводы типа Скайлэйк. (Больше инструкций / мопов занимают больше места в ROB, уменьшая, насколько далеко вперед может видеть то же самое окно с неупорядоченным порядком, а выполнение на самом деле взрывное, с остановками, которые, вероятно, составляют некоторые из менее чем 4 мопов / среднее значение по часам.)
Если foo
действительно использует push / pop RBX, то для задержки не так много выигрыша. Восстановление происходит непосредственно перед ret
, а не сразу после, вероятно, не имеет значения, если только не существует ret
неверного прогноза или пропуска I-кэша, который задерживает выбор кода по адресу возврата.
Большинство нетривиально функции сохраняют / восстанавливают RBX, поэтому зачастую не стоит полагать, что если оставить переменную в RBX, это на самом деле означает, что она действительно остается в регистре при вызове. (Хотя рандомизация выбора сохраняемых вызовом функций регистров может быть хорошей идеей для смягчения этого иногда.)
Так что да, push rdi
/ pop rax
будет более эффективным в этом случай, и это, вероятно, пропущенная оптимизация для крошечных неконечных функций, в зависимости от того, что делает foo
и баланс между дополнительной задержкой сохранения / перезагрузки для x
и большим количеством инструкций для сохранения / восстановления rbx
вызывающей стороны .
Метаданные для разматывания стека могут представлять изменения RSP здесь, точно так же, как если бы он использовал sub rsp, 8
, чтобы пролить / перезагрузить x
в слот стека. (Но компиляторы также не знают об этой оптимизации, используя push
для резервирования пространства и инициализации переменной. Какой компилятор C / C ++ может использовать команды pu sh pop для создания локальных переменных вместо просто увеличения esp один раз? . И выполнение этого для более чем одной локальной переменной приведет к увеличению метаданных стека .eh_frame
, потому что вы перемещаете указатель стека отдельно с каждым pu sh. Это не мешает компиляторам использовать push / pop, чтобы сохранить / восстановить сохраненные вызовом регистры.)
IDK, если бы стоило научить компиляторов искать эту оптимизацию
Возможно, это хорошая идея для всего функция, а не через один вызов внутри функции. И, как я уже сказал, он основан на предположении c о том, что foo
все равно сохранит / восстановит RBX. (Или оптимизация пропускной способности, если вы знаете, что задержка от x до возвращаемого значения не важна. Но компиляторы этого не знают и обычно оптимизируют для задержки).
Если вы начнете делать это пессимистическое предположение c в большом количестве кода (например, вокруг отдельных вызовов функций внутри функций), вы начнете получать больше случаев, когда RBX не сохраняется / восстанавливается, и вы могли бы воспользоваться.
Вы также не хотите этого дополнительного сохранения / восстановления push / pop в al oop, просто сохраняйте / восстанавливайте RBX вне l oop и используйте регистры с сохранением вызовов в циклах, которые выполняют вызовы функций. Даже без циклов, в общем случае большинство функций выполняет несколько вызовов функций. Эта идея оптимизации может быть применима, если вы действительно не используете x
между любыми вызовами, непосредственно перед первым и последним, в противном случае у вас есть проблема с поддержанием 16-байтового выравнивания стека для каждого call
если вы делаете один щелчок после вызова, перед другим вызовом.
Компиляторы не очень хороши в крошечных функциях в целом. Но это не очень хорошо для процессоров. Не встроенные вызовы функций влияют на оптимизацию в лучшие времена, , если только компиляторы не могут видеть внутренности вызываемого и делать больше предположений, чем обычно. Вызов не встроенной функции является неявным барьером памяти: вызывающий должен предположить, что функция может читать или записывать любые глобально доступные данные, поэтому все такие переменные должны синхронизироваться c с абстрактной машиной C. (Анализ Escape позволяет сохранять локальные значения в регистрах между вызовами, если их адрес не экранирован от функции.) Кроме того, компилятор должен предположить, что регистры с вызовом-сглаживанием все засорены. Это отстой для плавающей запятой в x86-64 System V, которая не имеет регистров XMM с сохранением вызовов.
Крошечные функции, такие как bar()
, лучше встроить в вызывающие. Компилировать с -flto
, поэтому в большинстве случаев это может происходить даже через границы файлов. (Указатели функций и границы разделяемой библиотеки могут победить это.)
Я думаю, что одна из причин, по которой компиляторы не удосужились попытаться выполнить эту оптимизацию, заключается в том, что для этого потребуется целая куча различных код во внутренних компонентах компилятора , отличный от обычного стека и кода выделения регистров, который знает, как сохранять регистры, сохраняемые вызовами, и использовать их.
то есть было бы много работы реализовать, и много кода, чтобы поддерживать, и если он получит чрезмерное энтузиазм c об этом, это может сделать хуже код.
А также, что это ( надеюсь) не имеет значения; если это имеет значение, вы должны вставить bar
в вызывающую программу или foo
в bar
. Это нормально, если не существует много различных bar
-подобных функций и foo
велико, и по какой-то причине они не могут встроить в своих вызывающих.