В чем преимущество наличия энергонезависимых регистров в соглашении о вызовах?


Я программирую JIT-компилятор и был удивлен, обнаружив, что так много регистров x86-64 являются энергонезависимыми (сохраненными вызываемыми) в соглашении о вызовах Win64. Мне кажется, что энергонезависимые регистры просто больше работают во всех функциях, которые могли бы использовать эти регистры. Это кажется особенно верным в случае числовых вычислений, когда вы хотите использовать много регистров в листовой функции, скажем, некоторого вида высоко оптимизированного умножения матрицы. Однако только 6 из них Например, 16 регистров SSE изменчивы, поэтому вам придется много разливать, если вам нужно использовать больше.

Так что да, я не понимаю. Какой здесь компромисс?

3 2

3 ответа:

Если регистры сохраняются вызывающим абонентом, то вызывающий абонент Всегда должен сохранять или перезагружать эти регистры вокруг вызова функции. Но если регистры сохраняются вызываемым абонентом, то вызываемый абонент должен сохранять только те регистры, которые он использует, и только тогда, когда он знает, что они будут использоваться (т. е., возможно, вообще не в сценарии раннего выхода). Недостаткомэтого соглашения является то, что вызываемый абонент не знает о вызывающем абоненте, поэтому он может сохранять регистры, которые все равно мертвы, но я предполагаю, что это рассматривается как меньшая проблема.

Преимуществом наличия регистров nonvolatile является: производительность .

Чем меньше данных перемещается, тем эффективнее процессор.

Чем больше регистров volatile, тем больше энергии требуется процессору.

Соглашение о вызовах Windows x86-64 только с 6 регистрами xmm с блокировкой вызовов-не очень хороший дизайн, вы правы. Большинство циклов SIMD (и многие скалярные циклы FP) не содержат вызовов функций, поэтому они ничего не получают от хранения своих данных в сохраненных регистрах вызовов. Сохранение / восстановление-это чистый недостаток, потому что это редко, чем любой из их абонентов использует это энергонезависимое состояние.

В x86-64 System V все векторные регистры являются Call-clobbered, что, возможно, слишком далеко. другой способ. Сохранение 1 или 2 вызовов было бы неплохо во многих случаях, особенно для кода, который выполняет некоторые вызовы функций математической библиотеки. (используйте gcc -fno-math-errno, чтобы простые были встроены лучше; иногда единственная причина, по которой они этого не делают, заключается в том, что им нужно установить errno на NaN.)

Связано: как было выбрано соглашение о вызове SysV x86-64: анализ размера кода и количества команд для компиляции GCC SPECint/SPECfp.


Для целых регов, имея некоторые из каждого является определенно хороший , и все "нормальные" соглашения о вызовах (для всех архитектур, а не только x86) действительно имеют смесь. это уменьшает общий объем работы, выполняемой разливом / восстановлением в вызывающих абонентах и вызываемых абонентах вместе взятых.

Принуждение вызывающего объекта к разливу / перезагрузке всего вокруг каждого вызова функции плохо сказывается на размере кода или производительности. Сохранение / восстановление некоторых сохраненных вызовов regs в начале / конце функции позволяет не-листовым функциям сохранять некоторые вещи в регистрах. callс.

Рассмотрим некоторый код, который вычисляет пару вещей, а затем делает cout << "result: " << a << "foo" << b*c << '\n'; это 4 вызова функций std::ostream operator<<, и они обычно не встроены. Сохранение адреса cout и локальных объектов, которые вы только что вычислили в энергонезависимых регистрах, означает, что вам нужны только дешевые инструкции mov reg,reg для настройки args для следующего вызова. (Или push в соглашении о вызове stack-args).

Но наличие некоторых регистров с блокировкой вызовов, которые можно использовать без сохранения, также очень важно. Функции,которым не нужны все архитектурные регистры, могут просто использовать регистры с блокировкой вызовов в качестве временных. Это позволяет избежать введения разлива/перезагрузки в критический путь для цепочек зависимостей вызывающего абонента (для очень маленьких абонентов), а также сохранения инструкций.

Иногда сложная функция сохраняет / восстанавливает некоторые регистры, сохраненные вызовом, просто чтобы получить больше полных регистров (как вы видите с XMM для хруста чисел). Это вообще стоит того; сохранение / восстановление энергонезависимые регистры вызывающего обычно лучше, чем заполнение/перезагрузка ваших собственных локальных переменных в стек, особенно если вам придется делать это внутри любого цикла.


Еще одна причина для регистров с блокировкой вызовов заключается в том, что обычно некоторые из ваших значений "мертвы" после вызова функции: они нужны только как args для функции. Вычисление их в регистрах Call-clobbered означает, что вам не нужно ничего сохранять/восстанавливать, чтобы освободить эти регистры, но и что ваш вызываемый абонент может также свободно использовать их. Это еще лучше в соглашениях о вызовах, передающих ARG в регистрах: вы можете вычислить входные данные непосредственно в регистрах, передающих arg. (И скопируйте любой из них в сохраненные вызовом реги или разлейте их в память стека, если они также нужны после функции.)

(я, как термины называют сохранившиеся и называют-затерт, а не абонента сохраняются и вызываемого абонента сохраняются. Последние термины подразумевают, что кто-то должен сохранить регистры, а не просто позволить мертвым значениям умереть. летучий / энергонезависимость-это неплохо, но эти термины также имеют другие технические значения, такие как ключевые слова C или в терминах flash и DRAM.)