Использование регистров процессора С++
В С++ локальные переменные всегда выделяются в стеке. Стек является частью разрешенной памяти, которую может занимать ваше приложение. Эта память хранится в вашей памяти (если ее не поместить на диск). Итак, компилятор С++ всегда создает код ассемблера, в котором хранятся локальные переменные в стеке?
Возьмем, к примеру, следующий простой код:
int foo( int n ) {
return ++n;
}
В коде ассемблера MIPS это может выглядеть так:
foo:
addi $v0, $a0, 1
jr $ra
Как вы можете видеть, мне не нужно было использовать стек вообще для n. Может ли компилятор С++ распознать это и напрямую использовать регистры процессора?
Изменить: Вау, большое спасибо за ваши почти неотложные и обширные ответы! Тело функции foo должно быть, конечно, return ++n;
, а не return n++;
.:)
Ответы
Ответ 1
Отказ от ответственности: я не знаю 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.)
Ответ 2
Да. Нет правила, что "переменные всегда выделяются в стеке". Стандарт С++ ничего не говорит о стеке. Он не предполагает, что существует стек или существуют регистры. Он просто говорит, как должен вести себя код, а не как его реализовать.
Компилятор хранит только переменные в стеке, когда это необходимо - когда им приходится, например, проходить мимо вызова функции, или если вы пытаетесь взять их адрес.
Компилятор не является глупым.;)
Ответ 3
Да, хороший оптимизирующий C/С++ оптимизирует это. И еще МНОГО: См. Здесь: Обзор компилятора Felix von Leitners.
Обычный компилятор C/С++ не поместит каждую переменную в стек. Проблема с вашей функцией foo()
может заключаться в том, что переменная может пройти через стек к функции (это определяет ABI вашей системы (аппаратное обеспечение/ОС)).
С ключевым словом C register
вы можете дать компилятору намек на то, что, вероятно, было бы хорошо хранить переменную в регистре. Пример:
register int x = 10;
Но помните: компилятор свободен не хранить x
в регистре, если он хочет!
Ответ 4
Ответ: да. Это зависит от компилятора, уровня оптимизации и целевого процессора.
В случае мип первые четыре параметра, если они маленькие, передаются в регистры, а возвращаемое значение возвращается в регистр. Таким образом, ваш пример не требует выделения ничего в стеке.
Собственно, правда чуждо, чем вымысел. В вашем случае параметр возвращается без изменений: возвращаемое значение имеет значение n перед оператором ++:
foo:
.frame $sp,0,$ra
.mask 0x00000000,0
.fmask 0x00000000,0
addu $2, $zero, $4
jr $ra
nop
Ответ 5
Так как ваш пример foo
- функция идентификации (он просто возвращает ее аргумент), мой компилятор С++ (VS 2008) полностью удаляет этот вызов функции. Если я изменю его на:
int foo( int n ) {
return ++n;
}
компилятор вставляет это с помощью
lea edx, [eax+1]
Ответ 6
Да, регистры используются в С++. MDR (регистры данных памяти) содержит данные, которые извлекаются и сохраняются. Например, чтобы получить содержимое ячейки 123, мы будем загружать значение 123 (в двоичном виде) в MAR и выполнять операцию выборки. Когда операция будет выполнена, копия содержимого ячейки 123 будет находиться в MDR. Чтобы сохранить значение 98 в ячейке 4, мы загружаем 4 в MAR и 98 в MDR и выполняем хранилище. Когда операция будет завершена, содержимое ячейки 4 будет установлено в 98, отбросив все, что было ранее. Регистры данных и адресов работают с ними для достижения этого. В С++ тоже, когда мы инициализируем var со значением или задаем его значение, происходят те же явления.
И, еще одна вещь, современные компиляторы также выполняют регистрацию, которая скорее быстрее, чем распределение памяти.