Рассмотрим следующий код:
class cFoo {
private:
int m1;
char m2;
public:
int doSomething1();
int doSomething2();
int doSomething3();
}
class cBar {
private:
cFoo mFoo;
public:
cFoo getFoo(){ return mFoo; }
}
void some_function_in_the_callstack_hierarchy(cBar aBar) {
int test1 = aBar.getFoo().doSomething1();
int test2 = aBar.getFoo().doSomething2();
...
}
В строке, где вызывается getFoo (), компилятор сгенерирует временный объект cFoo, чтобы иметь возможность вызывать doSomething1 ().
Использует ли компилятор стековую память, которая используется для этих временных объектов?
Сколько стековой памяти зарезервирует вызов some_function_in_the_callstack_hierarchy? Резервирует ли память для каждого сгенерированного временного?
Я предположил, что компилятор резервирует память только для одного объекта cFoo и будет повторно использовать память для разных вызовов, но если я добавлю
int test3 = aBar.getFoo().doSomething3();
Я вижу, что необходимый размер стека для «some_function_in_the_callstack_hierarchy» намного больше, и это не только из-за дополнительной локальной переменной int.
С другой стороны, если я тогда заменю
cFoo getFoo(){ return mFoo; }
со ссылкой (только для целей тестирования, потому что возвращение ссылки на приватный член не очень хорошо)
const cFoo& getFoo(){ return mFoo; }
ему требуется намного меньше стековой памяти, чем размер одного cFoo.
Поэтому для меня кажется, что компилятор резервирует дополнительную память стека для каждого сгенерированного временного объекта в функции. Но это было бы очень неэффективно.
Может кто-нибудь объяснить это?
оптимизирующий компилятор преобразовывает ваш исходный код в некоторое внутреннее представление и нормализует его.
С бесплатно программное обеспечение компиляторы (как НКУ & Clang / LLVM), вы можете посмотреть это внутреннее представление (по крайней мере, исправив код компилятора или запустив его в каком-либо отладчике).
Кстати, иногда временные значения даже не нуждаются в пространстве стека, например потому что они были оптимизированы, или потому что они могут сидеть в регистрах. И довольно часто они будут использовать какой-то ненужный слот в текущем кадре вызова. Также (особенно в C ++) много (маленьких) функций встраиваемый -как твой getFoo
вероятно, есть (так что у них самих нет фрейма вызова). Последние GCC даже иногда в состоянии хвост вызова оптимизация (по сути, повторное использование фрейма звонящего).
Если вы компилируете с GCC (т.е. g++
) Я бы предложил поиграть с варианты оптимизации а также варианты разработчика (и некоторые другие). Возможно использовать также -Wstack-usage=48
(или какое-то другое значение, в байтах на кадр вызова) и / или -fstack-usage
Во-первых, если вы можете прочитать ассемблерный код, скомпилируйте yourcode.cc
с g++ -S -fverbose-asm -O yourcode.cc
и смотреть в излучаемый yourcode.s
(не забудьте поиграть с флагами оптимизации, поэтому замените -O
с -O2
или же -O3
….)
Затем, если вам более интересно узнать, как оптимизируется компилятор, попробуйте g++ -O -fdump-tree-all -c yourcode.cc
и вы получите много так называемых «файлов дампа», которые содержат частичный текстовое представление внутренних представлений, имеющих отношение к GCC.
Если вам еще интереснее, загляните в мой GCC MELT и особенно его документация страница (которая содержит много слайдов & Рекомендации).
Поэтому для меня кажется, что компилятор резервирует дополнительную память стека для каждого сгенерированного временного объекта в функции.
Конечно, нет, в общем случае (и, конечно, если вы включите некоторые оптимизации). И даже если какое-то место зарезервировано, оно будет очень быстро использовано повторно.
Кстати: обратите внимание, что стандарт C ++ 11 не говорит о стеке. Можно представить себе какую-то программу на С ++, скомпилированную без использования какого-либо стека (например, оптимизация всей программы, обнаруживающая программу без рекурсии, чье пространство стека и компоновка могут быть оптимизированы, чтобы избежать какого-либо стека. Я не знаю такого компилятора, но я знаю, что компиляторы может быть довольно умным ….)
Попытка проанализировать как компилятор будет обрабатывать определенный фрагмент кода, который становится все сложнее, так как стратегии оптимизации становятся все более агрессивными.
Все, что нужно сделать компилятору — это реализовать стандарт C ++ и скомпилировать код, не вводя и не отменяя никаких побочных эффектов (за некоторыми исключениями, такими как возврат и оптимизация именованного возвращаемого значения).
Вы можете увидеть из своего кода, так как cFoo
не является полиморфным типом и не имеет данных-членов, компилятор может оптимизировать создание объекта в целом и вызвать то, что по существу, поэтому static
функционирует напрямую. Я полагаю, что даже во время написания этой статьи некоторые компиляторы уже это делают. Вы всегда можете проверить выходную сборку, чтобы быть уверенным.
Изменить: ОП теперь представил членов класса. Но так как они никогда не инициализируются и private
компилятор может удалить их, не задумываясь об этом. Этот ответ, следовательно, все еще применяется.
Время жизни временного объекта до конец полного содержащего выражения, см. параграф «12.2 Временные объекты» Стандарта.
Маловероятно, что даже при самых низких настройках оптимизации компилятор не будет повторно использовать пространство после окончания срока службы временного объекта.