Использование регистра ЦП C++

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

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

int foo( int n ) {
   return ++n;
}

В коде ассемблера MIPS это может выглядеть так:

foo:
addi $v0, $a0, 1
jr $ra

Как видите, мне вообще не нужно было использовать стек для n. Будет ли компилятор С++ распознавать это и напрямую использовать регистры ЦП?

Редактировать: Вау, большое спасибо за ваши почти немедленные и подробные ответы! Тело функции foo, конечно, должно быть return ++n;, а не return n++;. :)


person Johannes    schedule 02.12.2009    source источник
comment
Компилятор будет оптимизировать. Попробуйте gcc -fverbose-asm -O2 -S yoursource.c, а затем загляните внутрь yoursource.s   -  person Basile Starynkevitch    schedule 12.10.2012


Ответы (6)


Отказ от ответственности: я не знаю 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).

person int3    schedule 02.12.2009
comment
-O отключает настройку фрейма стека на машинах, где это не мешает отладке — x86 не является одним из них, вам нужен отдельный указатель -fomit-frame-point для устранения «избыточной» настройки фрейма стека (что на самом деле полезно для отладки). , т.е. в раскручивании кадра стека) - person matja; 02.12.2009
comment
да, я совсем забыл об этом. Я исправлю это. Но разница остается. - person int3; 02.12.2009
comment
Компилятор, выполняющий оптимизацию времени компоновки, также может распознать, что вызов можно превратить в быстрый вызов сам по себе, поскольку он может видеть и исправлять все сайты вызовов. - person Richard Pennington; 02.12.2009
comment
Какая разница осталась? Тот, на который указала Матья, был единственным неэффективным, не так ли? И это было вызвано отсутствием флага оптимизации. -1 за то, что вы не можете предположить, что компилятор будет хранить промежуточные продукты в регистрах. Вы правы с более сложной оптимизацией, конечно, но для этой? - person jalf; 02.12.2009
comment
Разница будет заключаться в том, что компилятор сделает эквивалент быстрого вызова без необходимости использования программистом нестандартного атрибута. - person Richard Pennington; 02.12.2009
comment
@jalf: ты читал обновленный пост? Я добавил флаг оптимизации задолго до того, как вы опубликовали этот комментарий. Очевидно, что в версии без fastcall есть еще одна инструкция. @Richard: я связал его вместе, и он все еще производил небыстрый вызов. - person int3; 02.12.2009
comment
@Richard: по общему признанию, в моем компиляторе нет нового оптимизатора времени ссылки gcc ... Интересно, будет ли это иметь значение. Но при более «распространенных» вариантах компиляции определенно есть разница. - person int3; 02.12.2009
comment
Да, есть еще одна инструкция. Это потому, что он должен использовать то же соглашение о вызовах, что и вызывающий. В противном случае фактически вызов функции невозможен. Эта дополнительная инструкция, очевидно, удаляется, если функция встроена (что обычно и происходит). Но, честно говоря, я думаю, что вы отвлеклись. Прочитайте собственно вопрос. Мой компилятор не игнорирует соглашения о вызовах для создания оптимального кода. Это был мой компилятор, способный хранить локальные переменные в регистрах. И параметр функции не совсем локальная переменная, поэтому он не в регистре. - person jalf; 03.12.2009
comment
Нелокальные переменные, переменные, которые должны быть доступны для других функций, должны соответствовать дополнительным ограничениям, таким как правильное соглашение о вызовах. Это не неэффективность и не отсутствие оптимизации. Он создает функцию, которая работает. - person jalf; 03.12.2009
comment
@jalf: я не уверен, что вы подразумеваете под «созданием работающей функции»; конечно, функция fastcall тоже работает, пока мы сообщаем вызывающей стороне, какое соглашение она использует? Но да, теперь, когда вы указали на это, я понимаю, что вопрос ОП в его примерах отличался от вопроса, который он изложил на словах, и я должен был решить обе проблемы. - person int3; 04.12.2009
comment
@jalf: я думаю, что мы спорили по поводу другого понимания рассматриваемого вопроса. Извините за путаницу. - person int3; 04.12.2009

Да. Нет правила, согласно которому «переменные всегда размещаются в стеке». Стандарт C++ ничего не говорит о стеке. Он не предполагает, что стек существует или существуют регистры. Он просто говорит, как должен вести себя код, а не как он должен быть реализован.

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

Компилятор не дурак. ;)

person jalf    schedule 02.12.2009

Да, хороший оптимизирующий C/C++ оптимизирует это. И даже НАМНОГО больше: см. здесь: Компилятор Felix von Leitners Опрос.

Обычный компилятор C/C++ все равно не поместит каждую переменную в стек. Проблема с вашей функцией foo() может заключаться в том, что переменная может быть передана через стек в функцию (это определяет ABI вашей системы (аппаратное обеспечение/ОС)).

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

register int x = 10;

Но помните: компилятор может не хранить x в регистре, если захочет!

person Johannes Weiss    schedule 02.12.2009

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

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

На самом деле, правда более странна, чем вымысел. В вашем случае параметр возвращается без изменений: возвращаемое значение равно значению n перед оператором ++:

foo:
    .frame  $sp,0,$ra
    .mask   0x00000000,0
    .fmask  0x00000000,0

    addu    $2, $zero, $4
    jr      $ra
    nop
person Richard Pennington    schedule 02.12.2009

Поскольку функция foo в вашем примере является функцией идентификации (она просто возвращает свой аргумент), мой компилятор C++ (VS 2008) полностью удаляет вызов этой функции. Если я изменю его на:

int foo( int n ) {
   return ++n;
}

компилятор встраивает это с помощью

lea edx, [eax+1] 
person Andreas Brinck    schedule 02.12.2009
comment
Да, снова на примере mips: static int foo( int n ) { return n++; } int fee() { return foo(5); } дает: .text .align 2 .globl fee .ent fee fee: .frame $sp,0,$ra .mask 0x00000000,0 .fmask 0x00000000,0 addiu $2, $zero, 5 jr $ra nop .set macro . установить повторный заказ .конечная плата .размер плата, .-плата - person Richard Pennington; 02.12.2009

Да, регистры используются в C++. MDR (регистры данных памяти) содержит извлекаемые и сохраняемые данные. Например, чтобы получить содержимое ячейки 123, мы должны загрузить значение 123 (в двоичном виде) в MAR и выполнить операцию выборки. Когда операция будет выполнена, копия содержимого ячейки 123 окажется в MDR. Чтобы сохранить значение 98 в ячейке 4, мы загружаем 4 в MAR и 98 в MDR и выполняем сохранение. Когда операция будет завершена, содержимое ячейки 4 будет установлено на 98 за счет отбрасывания всего, что было там ранее. Регистры данных и адресов работают с ними для достижения этой цели. В C++ тоже, когда мы инициализируем переменную значением или запрашиваем ее значение, происходят те же явления.

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

person Community    schedule 24.07.2017