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

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

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


person Trillian    schedule 01.05.2012    source источник
comment
Это не так, как это работает, вы генерируете свой собственный машинный код, поэтому вы устанавливаете свои собственные правила. Вам нужно только наблюдать abi x64 при вызове внешнего кода. Что в любом случае требует нестандартного маршаллера.   -  person Hans Passant    schedule 01.05.2012
comment
@HansPassant Хм, да, я не думал об этом. Это мой первый подобный проект, и я хочу вызвать внешний код, поэтому мне проще везде использовать Win64. Но я понимаю, что мог бы поступить иначе.   -  person Trillian    schedule 01.05.2012
comment
@HansPassant Означает ли это, что внутри ядра Linux он может переопределить эти правила, пока он придерживается того, что он решил делать внутри (поскольку они не высечены из камня и никоим образом не предусмотрены оборудованием)?   -  person HighOnMeat    schedule 25.12.2013


Ответы (4)


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

person hobbs    schedule 01.05.2012
comment
Итак, как только один из ваших вызываемых абонентов не использует энергонезависимый регистр, вы сохранили некоторую утечку / загрузку. Думаю, в этом есть смысл. Спасибо. - person Trillian; 01.05.2012

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

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

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


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

Принуждение вызывающей стороны проливать / перезагружать все вокруг каждого вызова функции не очень хорошо для размера кода или производительности. Сохранение / восстановление некоторых регистров с сохранением вызовов в начале / конце функции позволяет функциям, не являющимся листовыми, сохранять некоторые вещи в регистрах через calls.

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

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

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


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

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

person Peter Cordes    schedule 09.11.2017

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

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

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

person ncomputers    schedule 16.01.2016

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

Как правило, по крайней мере в соглашении о вызовах Microsoft x64, вы будете увидеть много явно сохраненных энергонезависимых регистров в стеке, но не явно сохраненных энергозависимых регистров - я думаю, идея состоит в том, что компиляторы никогда не доходят до стадии, когда вызывающей стороне необходимо явно сохранить регистр прямо перед вызовом, особенно выражение, которое не является это переменная в самой программе; вместо этого он может планировать заранее и полностью избегать использования этих регистров, использовать регистр, но не оптимизировать хранилище переменных вне стека, использовать регистры для параметров, переданных вызываемой функции, которые не работают после вызова вызываемой функции, потому что они не работают. t определены как переменные в программе или используют изменчивый регистр.

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

Я согласен с тем, что сохранение вызывающего абонента означает, что регистр необходимо сохранить независимо от того, использует его вызывающий абонент или нет. Это неверно, и, возможно, даже не нужно сохранять регистр, даже если он действительно использует регистр, потому что он знает, что он ему не нужен после вызова или может вообще не выполнять вызов.

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

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

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

person Lewis Kelsey    schedule 28.03.2021