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

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

void Dispatcher::processCommand() {
    ...
    displayMemory();
    this->process(this->instruction);
}

void Dispatcher::process(const Instruction &instruction) {
    displayMemory();
    if (instruction.length > 0) {
        ...
        return;
    }

    ErrorResponse::componentNotFound(instruction).output(this->serial);
    TerminationResponse(instruction.commandId, 0xFF).output(this->serial);
}

Первый вызов DisplayMemory() сообщит, что осталось 351 байт. Второй сообщит, что осталось всего 159 байт (то есть на 192 байта меньше).

Если я изменю тело функции процесса, разница между двумя отчетами о памяти изменится. Например, если я удалю две последние строки, разница уменьшится со 192 байт до 12 байт.

Я не понимаю, почему это происходит. Я думал, что между моментом, когда я готов вызвать функцию, и моментом, когда функция запускается, память должна использоваться исключительно дополнительным элементом в стеке вызовов (может быть, вышеупомянутыми 12 байтами?) и копированием параметров, передаваемых по значению (здесь нет). Почему это не так? Что на самом деле происходит под капотом?

, 👍1


2 ответа


Лучший ответ:

3

Ключ к разгадке кроется в этих последних двух строках:

    ErrorResponse::componentNotFound(instruction).output(this->serial);
    TerminationResponse(instruction.commandId, 0xFF).output(this->serial);

Давайте разорвем их на части и посмотрим, что они на самом деле делают.

ErrorResponse::componentNotFound(instruction).output(this->serial);

Вызовите статическую функцию componentNotFound с помощью вашей инструкции, затем возьмите возвращаемый объект и вызовите для него метод output(), передав this->serial в качестве параметра.

Подсказка там заключается в том, чтобы взять возвращаемый объект.

componentNotFound() возвращает объект. Этот объект должен где-то храниться. Это "где-то" находится в стеке. А пространство стека выделяется во время преамбулы функции. Это фрагмент кода, который выполняется в начале функции, выделяющей стек, помещающей регистры в стек и т.д.

То же самое происходит и во второй строке:

TerminationResponse(instruction.commandId, 0xFF).output(this->serial);

Создайте новый объект TerminationResponse и вызовите метод вывода.

Все это попадает в стек.

Это не вызов функции, которая использует память, это то, что делает функция, которая использует память.

,

2

Пространство памяти данных функции резервируется в стеке при вызове функции и возвращается при возврате функции. Если эта function1 вызывает другую function2, то место function1 остается в стеке, и больше места зарезервировано для function2 на время его выполнения. Когда функция 2 возвращается к функции 1, память функции 2 освобождается, и, наконец, когда функция 1 возвращается, ее память освобождается.

Теперь немного подробнее:

Когда вы вызываете функцию function1, аргументы, которые вы ей передаете, помещаются в стек. Когда управление фактически переходит к function1, адрес инструкции, следующий за вызовом, куда function1 в конечном итоге должен вернуться, помещается в стек. Если функции 1 требуется временное пространство данных для данных (автоматические переменные), место для них помещается в стек. (То же самое для функции 2, если она вызывается из функции 1; и так далее.)

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

Разница в использовании, которую вы видите в зависимости от наличия или отсутствия последних двух строк Dispatcher::process() , заключается в памяти (аргументы + адрес возврата + автоматические данные), используемой этими двумя функциями.

,