Использование регистров процессора С++

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

Возьмем, к примеру, следующий простой код:

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 со значением или задаем его значение, происходят те же явления.

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