Использование регистра ЦП C++
-
11-09-2019 - |
Вопрос
В C++ локальные переменные всегда размещаются в стеке.Стек — это часть разрешенной памяти, которую может занимать ваше приложение.Эта память хранится в вашей оперативной памяти (если не выгружена на диск).Итак, всегда ли компилятор C++ создает ассемблерный код, который хранит локальные переменные в стеке?
Возьмем, к примеру, следующий простой код:
int foo( int n ) {
return ++n;
}
В ассемблерном коде MIPS это может выглядеть так:
foo:
addi $v0, $a0, 1
jr $ra
Как видите, мне вообще не пришлось использовать стек для n.Будет ли компилятор C++ распознавать это и напрямую использовать регистры ЦП?
Редактировать: Ого, большое спасибо за ваши почти немедленные и обширные ответы!Тело функции foo, конечно, должно быть return ++n;
, нет return n++;
. :)
Решение
Отказ от ответственности:Я не знаю MIPS, но знаю немного x86, и думаю, принцип должен быть тот же..
В обычном соглашении о вызове функций компилятор помещает значение n
в стек, чтобы передать его в функцию foo
.Однако существует fastcall
соглашение, которое вы можете использовать, чтобы указать gcc вместо этого передавать значение через регистры.(В MSVC также есть эта опция, но я не уверен, каков ее синтаксис.)
test.cpp:
int foo1 (int n) { return ++n; }
int foo2 (int n) __attribute__((fastcall));
int foo2 (int n) {
return ++n;
}
Компиляция вышеизложенного с помощью g++ -O3 -fomit-frame-pointer -c test.cpp
, я получу за foo1
:
mov eax,DWORD PTR [esp+0x4]
add eax,0x1
ret
Как видите, он считывает значение из стека.
И вот foo2
:
lea eax,[ecx+0x1]
ret
Теперь он берет значение прямо из регистра.
Конечно, если вы встроите функцию, компилятор выполнит простое добавление в тело вашей более крупной функции, независимо от указанного вами соглашения о вызовах.Но когда вы не можете внедрить его, это произойдет.
Отказ от ответственности 2:Я не говорю, что вам следует постоянно сомневаться в компиляторе.Вероятно, в большинстве случаев это непрактично и необходимо.Но не думайте, что он создает идеальный код.
Редактировать 1: Если вы говорите о простых локальных переменных (а не об аргументах функции), то да, компилятор разместит их в регистрах или в стеке по своему усмотрению.
Редактировать 2: Похоже, что соглашение о вызовах зависит от архитектуры, и MIPS передаст первые четыре аргумента в стек, как заявил Ричард Пеннингтон в своем ответе.Итак, в вашем случае вам не нужно указывать дополнительный атрибут (который на самом деле является атрибутом, специфичным для x86).
Другие советы
Да.Не существует правила, согласно которому «переменные всегда размещаются в стеке».Стандарт C++ ничего не говорит о стеке. Он не предполагает, что стек существует или существуют регистры.Он просто говорит о том, как должен вести себя код, а не о том, как его следует реализовать.
Компилятор сохраняет переменные в стеке только тогда, когда это необходимо - например, когда им приходится пережить вызов функции или если вы пытаетесь получить их адрес.
Компилятор не глупый.;)
Да, хороший оптимизирующий C/C++ оптимизирует это.И даже МНОГО более: Глянь сюда:Опрос компилятора Феликса фон Лейтнерса.
Обычный компилятор C/C++ в любом случае не поместит в стек каждую переменную.Проблема с твоим foo()
Функция может заключаться в том, что переменная может быть передана через стек в функцию (это определяет ABI вашей системы (аппаратное обеспечение/ОС).
С буквами C register
ключевое слово, которое вы можете дать компилятору намекать что, вероятно, было бы хорошо хранить переменную в регистре.Образец:
register int x = 10;
Но помни:Компилятор может не хранить x
в реестр, если захочет!
Ответ: да, возможно.Это зависит от компилятора, уровня оптимизации и целевого процессора.
В случае mips первые четыре параметра, если они малы, передаются в регистрах, а возвращаемое значение возвращается в регистре.Итак, в вашем примере нет необходимости выделять что-либо в стеке.
На самом деле правда страннее вымысла.В вашем случае параметр возвращается без изменений:возвращаемое значение равно значению n перед оператором ++:
foo:
.frame $sp,0,$ra
.mask 0x00000000,0
.fmask 0x00000000,0
addu $2, $zero, $4
jr $ra
nop
Поскольку ваш пример foo
function является функцией идентификации (она просто возвращает свой аргумент), мой компилятор C++ (VS 2008) полностью удаляет этот вызов функции.Если я изменю его на:
int foo( int n ) {
return ++n;
}
компилятор встраивает это с помощью
lea edx, [eax+1]
Да, регистры используются в C++.MDR (регистры данных памяти) содержат данные, которые извлекаются и сохраняются.Например, чтобы получить содержимое ячейки 123, мы должны загрузить значение 123 (в двоичном формате) в MAR и выполнить операцию выборки.Когда операция будет завершена, копия содержимого ячейки 123 окажется в MDR.Чтобы сохранить значение 98 в ячейке 4, мы загружаем 4 в MAR и 98 в MDR и выполняем сохранение.Когда операция завершится, содержимому ячейки 4 будет присвоено значение 98, путем отбрасывания всего, что было там ранее.Для достижения этой цели совместно с ними работают регистры данных и адресов.В C++, когда мы инициализируем переменную значением или запрашиваем ее значение, происходит то же самое.
И еще: современные компиляторы также выполняют распределение регистров, что происходит быстрее, чем выделение памяти.