Подтвердить что ты не робот

Как известно, что переменные находятся в регистре или в стеке?

Я читаю этот вопрос о inline в часто задаваемых вопросах isocpp, код указан как

void f()
{
  int x = /*...*/;
  int y = /*...*/;
  int z = /*...*/;
  // ...code that uses x, y and z...
  g(x, y, z);
  // ...more code that uses x, y and z...
 }

то он говорит, что

Предполагая типичную реализацию на С++, которая имеет регистры и стек, регистры и параметры записываются в стек непосредственно перед вызов g(), тогда параметры считываются из стека внутри g() и снова прочитайте, чтобы восстановить регистры, пока g() возвращается в f(). Но это много ненужного чтения и письма, особенно в случаях, когда компилятор может использовать регистры для переменных x, y и z: каждая переменная может быть записана дважды (как регистр и также как параметр) и дважды читайте (при использовании в g() и восстановите регистры во время возврата к f()).

Мне трудно понять абзац выше. Я пытаюсь перечислить свои вопросы, как показано ниже:

  • Для того, чтобы компьютер мог выполнять некоторые операции над некоторыми данными, которые находятся в основной памяти, верно ли, что данные должны сначала загружаться в некоторые регистры, а затем ЦП может работать с данными? (Я знаю, что этот вопрос не имеет особого отношения к С++, но понимание этого будет полезно для понимания того, как работает С++.)
  • Я думаю, что f() - это функция, аналогичная функции g(x, y, z). Почему x, y, z перед вызовом g() находятся в регистрах, а параметры, переданные в g(), находятся в стеке?
  • Как известно, что объявления для x, y, z делают их сохраненными в регистрах? Где хранятся данные внутри g(), регистрируются или стекаются?

PS

Очень сложно выбрать приемлемый ответ, когда ответы все очень хорошие (например, те, что предоставлены @MatsPeterson, @TheodorosChatzigiannakis и @superultranova). Я думаю. Лично мне нравится @Potatoswatter немного больше, так как ответ предлагает некоторые рекомендации.

4b9b3361

Ответ 1

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

Но ваши вопросы очень хорошие.

  • Для того, чтобы компьютер мог выполнять некоторые операции над некоторыми данными, которые находятся в основной памяти, верно ли, что данные должны сначала загружаться в некоторые регистры, а затем ЦП может работать с данными? (Я знаю, что этот вопрос не имеет особого отношения к С++, но понимание этого будет полезно для понимания того, как работает С++.)

Больше или меньше, все должно быть загружено в регистры. Большинство компьютеров организованы вокруг datapath, шины, соединяющей регистры, арифметические схемы и верхний уровень иерархии памяти. Обычно все, что передается в датапате, идентифицируется с помощью регистра.

Вы можете вспомнить великую дискуссию RISC и CISC. Одним из ключевых моментов было то, что дизайн компьютера может быть намного проще, если памяти не разрешено напрямую подключаться к арифметическим схемам.

В современных компьютерах существуют архитектурные регистры, которые представляют собой конструкцию программирования, такую ​​как переменная и физические регистры, которые являются актуальными схемами. Компилятор делает много тяжелой работы, чтобы отслеживать физические регистры при создании программы с точки зрения архитектурных регистров. Для команды CISC, такой как x86, это может включать в себя инструкции генерации, которые передают операнды в памяти непосредственно в арифметические операции. Но за кулисами он регистрируется полностью.

Нижняя строка: просто дайте компилятору сделать что-то.

  1. Я считаю, что функция f() является функцией, аналогичной функции g (x, y, z). Почему x, y, z перед вызовом g() находятся в регистрах, а параметры, переданные в g(), находятся в стеке?

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

Нижняя строка: пример произвольно предполагает наивный ABI.

  1. Как известно, что объявления для x, y, z делают их сохраненными в регистрах? Где хранятся данные внутри g(), регистрируются или стекаются?

Компилятор предпочитает использовать регистры для более часто используемых значений. Ничто в этом примере не требует использования стека. Однако менее часто используемые значения будут помещены в стек, чтобы сделать доступными регистры.

Только когда вы берете адрес переменной, например, &x или передаете по ссылке, и этот адрес выходит из Inliner, требуется компилятор использовать память, а не регистры.

Нижняя строка: избегайте приема адресов и передачи/хранения их волей-неволей.

Ответ 2

Это полностью зависит от компилятора (в сочетании с типом процессора), сохраняется ли переменная в памяти или в регистре [или в некоторых случаях более одного регистра] (и какие параметры вы предоставляете компилятору, предполагая, что она получена варианты для решения таких вещей - большинство "хороших" компиляторов делают). Например, компилятор LLVM/Clang использует специальный прогон оптимизации, называемый mem2reg, который перемещает переменные из памяти в регистры. Решение сделать это основано на том, как используются переменные - например, если вы берете адрес переменной в какой-то момент, она должна быть в памяти.

Другие компиляторы имеют схожие, но не обязательно идентичные функциональные возможности.

Кроме того, по крайней мере, в компиляторах, которые имеют некоторое подобие переносимости, ТАКЖЕ будет фазой машинного кода generatinc для фактической цели, которая содержит целевые оптимизации, которые снова могут перемещать переменную из памяти в регистр.

Невозможно [без понимания того, как работает конкретный компилятор], чтобы определить, находятся ли переменные в вашем коде в регистре или в памяти. Можно догадаться, но такое предположение похоже на угадывание других "предсказуемых вещей", например, глядя в окно, чтобы догадаться, будет ли дождь через несколько часов - в зависимости от того, где вы живете, это может быть полная случайная догадка, или вполне предсказуемые - некоторые тропические страны, вы можете установить часы на основе того, когда дождь прибывает каждый день, в другие страны, редко идет дождь, а в некоторых странах, как здесь, в Англии, вы не можете знать наверняка за пределами "прямо сейчас [дождь] дождь здесь".

Чтобы ответить на актуальные вопросы:

  • Это зависит от процессора. Правильные RISC-процессоры, такие как ARM, MIPS, 29K и т.д., Не имеют инструкций, в которых используются операнды памяти, кроме инструкций типа загрузки и типа хранилища. Поэтому, если вам нужно добавить два значения, вам нужно загрузить значения в регистры и использовать операцию добавления этих регистров. Некоторые, такие как x86 и 68K, позволяют одному из двух операндов быть операндом памяти, и, например, PDP-11 и VAX имеют "полную свободу", независимо от того, находятся ли ваши операнды в памяти или регистре, вы можете использовать ту же инструкцию, просто различные режимы адресации для разных операндов.
  • Ваше первоначальное предположение здесь неверно - это не гарантирует, что аргументы g находятся в стеке. Это всего лишь один из многих вариантов. Многие ABI (бинарный интерфейс приложения, так называемые "соглашения о вызовах" ) используют регистры для первых нескольких аргументов функции. Таким образом, опять же, это зависит от того, какой компилятор (в некоторой степени) и какой процессор (намного больше, чем какой компилятор) цели компилятора являются ли аргументы в памяти или в регистрах.
  • Опять же, это решение, которое делает компилятор - это зависит от того, сколько регистров имеет процессор, какие доступны, какая стоимость означает "освобождение" некоторого регистра для x, y и z - от "без каких-либо затрат" до "совсем немного" - опять же, в зависимости от модели процессора и ABI.

Ответ 3

Чтобы компьютер мог выполнять некоторые операции над некоторыми данными, которые находятся в основной памяти, верно ли, что данные должны сначала загружаться в некоторые регистры, тогда ЦП может работать с данными?

Даже это утверждение всегда верно. Вероятно, это верно для всех платформ, с которыми вы когда-либо работаете, но, безусловно, может быть другая архитектура, которая не использует регистры процессора вообще.

Однако ваш компьютер x86_64.

Я считаю, что функция f() является функцией, аналогичной функции g (x, y, z). Почему x, y, z перед вызовом g() находятся в регистрах, а параметры, переданные в g(), находятся в стеке?

Как известно, что объявления для x, y, z делают их сохраненными в регистрах? Где хранятся данные внутри g(), регистрируются или стекаются?

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

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

Во-вторых, распределение регистров (т.е. какие переменные фактически загружаются в регистр в любой момент времени) является сложной задачей и NP-complete. Составители стараются изо всех сил использовать информацию, которую они имеют. В общем случае в стек попадают менее часто используемые переменные, а более часто используемые переменные хранятся в реестрах. Таким образом, часть Where the data inside g() is stored, register or stack? не может быть получена раз и навсегда, поскольку она зависит от многих факторов, включая давление в регистре.

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

Наконец, в связанном вами вопросе уже говорится

Естественно, что ваш пробег может быть разным, и существует множество переменных, которые выходят за рамки данного конкретного FAQ, но приведенное выше служит примером того, что может происходить с процедурной интеграцией.

то есть. в опубликованном вами параграфе сделаны некоторые предположения, чтобы установить пример для примера. Это просто предположения, и вы должны относиться к ним как таковым.

В качестве небольшого дополнения: в отношении преимуществ inline для функции я рекомендую взглянуть на этот ответ: fooobar.com/questions/18919/...

Ответ 4

Вы не можете знать, не глядя на язык ассемблера, является ли переменная в регистре, стеке, куче, глобальной памяти или в другом месте. Переменная - абстрактное понятие. Компилятору разрешено использовать регистры или другую память по мере выбора, если исполнение не изменяется.

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

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

Ответ 5

Что касается вашего вопроса №1, да, инструкции без нагрузки/хранения работают на регистрах.

Что касается вашего вопроса №2, если мы предполагаем, что параметры переданы в стеке, тогда мы должны записать регистры в стек, иначе g() не сможет получить доступ к данным, так как код в g() не "знает", который регистрирует параметры.

Что касается вашего вопроса №3, неизвестно, что x, y и z наверняка будут сохранены в регистре в f(). Можно использовать ключевое слово register, но это скорее предложение. Основываясь на соглашении о вызове и предполагая, что компилятор не выполняет никакой оптимизации, связанной с передачей параметров, вы можете предсказать, находятся ли параметры в стеке или в регистрах.

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

stdcall, cdecl и fastcall - некоторые примеры вызовов конвенций. Что касается передачи параметров, то stdcall и cdecl одинаковы, в параметрах они сдвигаются справа налево в стек. В этом случае, если g() был cdecl или stdcall, вызывающий нажимал z, y, x в следующем порядке:

mov eax, z
push eax
mov eax, x
push eax
mov eax, y
push eax
call g

В 64bit fastcall используются регистры, microsoft использует RCX, RDX, R8, R9 (плюс стек для функций, требующих более 4 параметров), linux использует RDI, RSI, RDX, RCX, R8, R9. Для вызова g() с использованием MS 64bit fastcall можно было бы сделать следующее (мы предполагаем z, x, а y не входят в регистры)

mov rcx, x
mov rdx, y
mov r8, z
call g

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

f:
xor rcx, rcx
mov rsi, x
mov r8, z
mov rdx y
call g
mov rcx, rax
ret

g:
mov rax, rsi
add rax, rcx
add rax, rdx
ret

Для иллюстративных целей rcx уже используется, а x загружен в rsi. Компилятор может скомпилировать g так, что он использует rsi вместо rcx, поэтому значения не должны меняться между двумя регистрами, когда наступает время вызова g. Компилятор также мог бы встроить g, теперь, когда f и g используют один и тот же набор регистров для x, y и z. В этом случае команда call g будет заменена содержимым g, за исключением команды ret.

f:
xor rcx, rcx
mov rsi, x
mov r8, z
mov rdx y
mov rax, rsi
add rax, rcx
add rax, rdx
mov rcx, rax
ret

Это будет еще быстрее, потому что нам не нужно иметь дело с инструкцией call, так как g был встроен в f.

Ответ 6

Короткий ответ: вы не можете. Это полностью зависит от вашего компилятора и включенных функций оптимизации.

Проблема компилятора заключается в том, чтобы перевести на сборку вашей программы, но как это делается, связано с тем, как работает ваш компилятор. Некоторые компиляторы позволяют вам подсказать, какую переменную карту нужно зарегистрировать. Проверьте, например, следующее: https://gcc.gnu.org/onlinedocs/gcc/Global-Reg-Vars.html

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

Ответ 7

Чтобы компьютер мог выполнять некоторые операции над некоторыми данными, которые находятся в основной памяти, верно ли, что данные должны сначала загружаться в некоторые регистры, тогда ЦП может работать с данными?

Это зависит от архитектуры и набора команд, которые она предлагает. Но на практике да - это типичный случай.

Как известно, что объявления для x, y, z делают их сохраненными в регистрах? Где хранятся данные внутри g(), регистрируются или стекаются?

Предполагая, что компилятор не устранит локальные переменные, он предпочитает помещать их в регистры, потому что регистры быстрее, чем стек (который находится в основной памяти или кеше).

Но это далеко не универсальная истина: она зависит от (сложной) внутренней работы компилятора (чьи данные описаны в этом параграфе).

Я считаю, что функция f() является функцией, аналогичной функции g (x, y, z). Почему x, y, z перед вызовом g() находятся в регистрах, а параметры, переданные в g(), находятся в стеке?

Даже если мы предположим, что переменные фактически хранятся в регистрах, когда вы вызываете функцию, соглашение о вызове запускается в Это соглашение, которое описывает, как вызывается функция, где передаются аргументы, кто очищает стек, какие регистры сохраняются.

Все вызывающие соглашения имеют какие-то накладные расходы. Одним из источников этих накладных расходов является передача аргументов. Многие соглашения о звонках пытаются уменьшить это, предпочитая передавать аргументы через регистры, но поскольку количество регистров процессора ограничено (по сравнению с пространством стека), они в конце концов возвращаются к проходу через стек после нескольких аргументов.

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

Ответ 8

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

Это по дизайну - для повышения производительности, поскольку для процессора проще и быстрее обрабатывать данные в регистрах. Архитектурные регистры ограничены по размеру, поэтому все не может быть помещено в регистры. Даже если вы "намекаете" на компилятор, чтобы записать его в регистр, в конечном итоге ОС может управлять им за пределами регистра в основной памяти, если доступные регистры заполнены.

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