Вызов функции C против Новые блоки для ввода / вывода в стек

#c #stack #block #function-calls

#c #стек #блок #вызовы функций

Вопрос:

Я читал о области видимости переменных в C и столкнулся с интересной блочной структурой:

 int main(int argc, char **argv) {
    int local;

    { // New level of scope
        int more_local;
    }

    return 0;
}
  

Я понимаю, что переменные извлекаются из стека в конце каждого блока, обозначаемого закрывающей фигурной скобкой } .

Я также читал, что вызовы функций также помещают свои переменные в стек и завершаются в конце вызова, обозначаемого закрывающей фигурной скобкой } :

 void foo() {
    int more_local;
}

int main(int argc, char **argv) {
    int local;
    foo();

    return 0;
}
  

Как по-разному обрабатывается стек в обеих ситуациях и каковы преимущества и недостатки обеих?

Ответ №1:

С помощью вызова функции вы помещаете адрес возврата в стек и создаете новый фрейм стека. Если вы просто заключаете части кода в фигурные скобки, вы определяете новую область видимости, как вы сказали. Они похожи на любой блок кода, следующий за управляющим оператором, таким как if, for, while и т.д.

Здесь нельзя говорить о преимуществах и недостатках, потому что это две совершенно разные вещи. Существует не так много ситуаций, в которых вам будет выгодно заключать блоки кода в фигурные скобки, и это может затруднить чтение кода.

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

1. Если я скомпилирую оба приведенных выше примера, я бы предположил, что пример с новой областью действия будет микрооптимизирован в отличие от вызова функции из-за дополнительных накладных расходов, связанных с созданием нового фрейма стека. Разве это не преимущество? У меня могут быть некоторые ошибки в моей логике, но, похоже, разница в производительности есть (независимо от того, насколько она мала).

2. Конечно, если ваша цель — только создать новую область видимости, тогда используйте фигурные скобки. Но почему вы хотите это сделать? Если это для повторного использования имен переменных, я бы предостерег от этого. Если это попытка оптимизировать использование вашей памяти, я бы не стал пытаться управлять этим таким образом; компилятор должен быть в состоянии оптимизировать это достаточно хорошо.

3. @Gio: В зависимости от уровня оптимизации ( /O1 должно быть уже достаточно), этот вызов функции, скорее всего, будет встроенным, в зависимости от его фактической длины и прочего.

Ответ №2:

Ну, вы могли бы сказать, что ваш первый пример можно рассматривать как встроенную функцию. 😛
Но, как правило, вызовы функций и открытие нового scope не имеют ничего общего друг с другом.
Когда вы вызываете функцию, адрес возврата и все аргументы помещаются в накопленный файл и извлекаются из него после возврата функции.
При открытии нового scope вы просто вызываете деструктор всех объектов в пределах этой области в конце его; ни в коем случае не гарантируется, что фактическое пространство, занимаемое этими переменными, сразу же извлекается из стека. Это возможно, но пространство также может быть просто повторно использовано другими переменными в функции, в зависимости от прихотей компиляторов / оптимизаторов.

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

1. Я думаю, вы имеете в виду конструктор, а не деструктор, поскольку деструктор вызывается в конце области видимости.

2. Я только что прочитал о встроенных функциях : «Когда компилятор выполняет встроенное расширение вызова функции, код функции вставляется в поток кода вызывающего» Если бы я встроил функцию второго примера, отличалась бы она от первого примера?

3. Осторожно, компилятор выполняет встроенное расширение вызова функции, а не вы. Даже если вы используете ключевое слово inline , это всего лишь подсказка компилятору. Функция может быть встроена во время компиляции, а может и не быть, в зависимости от того, что в конечном итоге решит компилятор.

Ответ №3:

int more_local; в обоих случаях они будут помещены в стек. Но второй сценарий будет иметь накладные расходы на вызов функции.

Я бы посоветовал вам подумать об этом:

 void foo()
{
    int local;

    { // New level of scope
        int more_local_1;
    }
    { // New level of scope
        int more_local_2;
    }
}
  

Здесь more_local_1 и more_local_2 могут использоваться одни и те же ячейки памяти. Когда-то он использовался для
more_local_1 и во второй области видимости для more_local_2 переменной.

Ответ №4:

  • локальные области по-прежнему могут обращаться к другим локальным переменным, в то время как функциям должна быть явно передана любая из переменных вызывающего объекта, которую они должны использовать

    • передача переменных — это проблема, но иногда это делает код более понятным, если четко указать меньший набор переменных, фактически необходимых для операций с ограниченной областью действия (а также поощрять группирование операций в отдельные функциональные блоки с присвоением соответствующего имени функции и имен параметров, соответствующих контексту, и затем они готовы к повторному использованию)
  • вызовы функций вне очереди требуют несколько других затрат места в стеке и производительности: адреса возврата, сохраненные регистры, инструкции по вызову и возврату

  • по сравнению с областью действия функции локальные области особенно хороши для минимизации области действия переменных, содержащих важные ресурсы, такие как большие объемы памяти, потоки, файловые дескрипторы и / или блокировки: чем выше уровень и дольше выполняется функция, тем полезнее ее быстрая очистка

    • уменьшенное время жизни переменной также уменьшает количество параллельных переменных, которые программист должен мысленно «отслеживать», чтобы понять и поддерживать код: чем меньше, тем лучше
  • иногда не имеет особого смысла выбирать произвольные разные идентификаторы, когда вы выполняете набор аналогичных операций, поэтому некоторые локальные области позволяют удобно «перерабатывать» идентификатор

  • локальные области немного неуклюжи и занимают «экранное пространство» в исходном коде, а также увеличивают уровень отступов, поэтому рекомендуется использовать их при наличии конкретного обоснования, а не по принципу «когда вы можете»

Ответ №5:

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