Как компилятор определяет необходимый размер стека для функции с временными данными, сгенерированными компилятором?

#c #compiler-construction #stack #temporaries

#c #компилятор-конструкция #стек #временные

Вопрос:

Рассмотрим следующий код:

 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 cFooamp; getFoo(){ return mFoo; }
 

для этого требуется намного меньше стековой памяти, чем размер одного CFoo.

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

Комментарии:

1. Это в значительной степени определяется реализацией. Даже не гарантируется, что объекты с автоматическим хранением будут созданы в стеке в первую очередь.

2. На всякий случай, если вы не убедились, что компилируете с чем-то вроде -O2 enabled. Анализ неоптимизированной сборки не очень полезен.

3. Он компилируется с помощью -O1

Ответ №1:

Оптимизирующий компилятор преобразует ваш исходный код в некоторое внутреннее представление и нормализует его.

С помощью компиляторов свободного программного обеспечения (таких как GCC amp; 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.

Если вам еще более любопытно, загляните в my GCC MELT и, в частности, на его страницу документации (которая содержит много слайдов и ссылок).

Поэтому мне кажется, что компилятор резервирует дополнительную память стека для каждого сгенерированного временного объекта в функции.

Конечно, нет, в общем случае (и, конечно, при условии, что вы включите некоторые оптимизации). И даже если какое-то место будет зарезервировано, оно будет очень быстро использовано повторно.

Кстати: обратите внимание, что стандарт C 11 не говорит о стеке. Можно представить себе некоторую программу на C , скомпилированную без использования какого-либо стека (например, оптимизация всей программы, обнаруживающая программу без рекурсии, пространство и расположение стека которой можно было бы оптимизировать, чтобы избежать любого стека. Я не знаю ни одного такого компилятора, но я знаю, что компиляторы могут быть довольно умными ….)

Ответ №2:

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

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

Из вашего кода видно, что, поскольку cFoo он не является полиморфным типом и не имеет данных-членов, компилятор может полностью оптимизировать создание объекта и вызывать то, что, по сути, является static функциями напрямую. Я бы предположил, что даже на момент моего написания некоторые компиляторы уже это делают. Вы всегда можете проверить выходную сборку, чтобы быть уверенным.

Редактировать: В OP теперь введены члены класса. Но поскольку они никогда не инициализируются и являются private , компилятор может удалить их, не слишком задумываясь об этом. Поэтому этот ответ остается в силе.

Комментарии:

1. Я забыл добавить некоторые элементы в CFoo. Я отредактировал вопрос, чтобы экземпляру CFoo требовалась память для переменных-членов.

2. Спасибо, что признали недействительным мой ответ ;-). Это все еще применяется, поскольку переменные никогда не инициализируются.

3. Это было бы верно только в том случае, если они никогда не используются. Но они используются в функции doSomething . Имейте в виду, что этот код предназначен только для вопроса. Я не могу показать вам свой реальный код, потому что он принадлежит моему работодателю

Ответ №3:

Время жизни временного объекта истекает до конца полного содержащего выражения, см. параграф «12.2 Временные объекты» Стандарта.

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