Как сохранить пространство стека с помощью хорошего дизайна?

StackOverflow https://stackoverflow.com/questions/135262

Вопрос

Я программирую на C для встроенного микроконтроллера с ограниченным объемом оперативной памяти и RTOS.

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

Есть ли альтернатива, позволяющая сохранить код хорошо организованным и читаемым, сохраняя при этом память?

Это было полезно?

Решение

Постарайтесь сделать стек вызовов более плоским, чтобы вместо a() звоню b() который вызывает c() который вызывает d(), иметь a() вызов b(), c(), и d() сам.

Если на функцию ссылаются только один раз, отметьте ее inline (при условии, что ваш компилятор поддерживает это).

Другие советы

Использование стека состоит из трех компонентов:

  • Адреса возврата вызова функции
  • Параметры вызова функции
  • автоматические (локальные) переменные

Ключом к минимизации использования стека является минимизация передачи параметров и автоматических переменных.Затраты места на сам вызов функции довольно минимальны.

Параметры

Один из способов решения проблемы с параметрами — передать структуру (через указатель) вместо большого количества параметров.


foo(int a, int b, int c, int d)
{
...
   bar(int a, int b);
}

вместо этого сделайте это:


struct my_params {
   int a;
   int b;
   int c;
   int d;
};
foo(struct my_params* p)
{
   ...
   bar(p);
};

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

Автоматические переменные (локальные)

Это, как правило, самый большой потребитель стекового пространства.

  • Массивы — убийцы.Не определяйте массивы в своих локальных функциях!
  • Минимизируйте количество локальных переменных.
  • Используйте наименьший необходимый тип.
  • Если повторный вход не является проблемой, вы можете использовать статические переменные модуля.

Имейте в виду, что если вы просто перемещаете все локальные переменные из локальной области в область модуля, вы НЕ сэкономите место.Вы обменяли пространство стека на пространство сегмента данных.

Некоторые RTOS поддерживают локальное хранилище потоков, которое выделяет «глобальное» хранилище для каждого потока.Это может позволить вам иметь несколько независимых глобальных переменных для каждой задачи, но это сделает ваш код не таким простым.

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

В C все переменные, объявленные внутри функции, «управляются автоматически», что означает, что они размещаются в стеке.

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

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

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

В GCC попробуйте добавить флаг «-finline-functions» (или -O3) и, возможно, флаг «-finline-limit=n».

Один трюк, который я где-то прочитал, чтобы оценить требования к стеку кода во встроенной установке, заключается в том, чтобы в начале заполнить пространство стека известным шаблоном (мой любимый шаблон - DEAD в шестнадцатеричном формате) и дать системе поработать некоторое время.

После нормального запуска прочтите пространство стека и посмотрите, какая часть пространства стека не была заменена в ходе работы.Спроектируйте так, чтобы оставить по крайней мере 150 % из этого, чтобы охватить все неясные пути кода, которые, возможно, не были использованы.

Можете ли вы заменить некоторые локальные переменные глобальными?В частности, массивы могут съедать стек.

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

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

Какие переменные есть в ваших функциях?О каких размерах и пределах идет речь?

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

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

Другая область, которая потребляет стек, — это локальные параметры.Эту область вы имеете некоторый контроль.Использование статики (на уровне файла) позволит избежать выделения стека за счет статического распределения оперативной памяти.Глобалы тоже.

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

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

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

Да, RTOS действительно может потреблять оперативную память для использования стека задач.Мой опыт показывает, что у новых пользователей ОСРВ возникает тенденция использовать больше задач, чем необходимо.

Для встраиваемой системы, использующей ОСРВ, ОЗУ может быть ценным товаром.Чтобы сохранить оперативную память, для простых функций по-прежнему может быть эффективно реализовать несколько функций в одной задаче, работающей по циклическому принципу, с совместной многозадачной конструкцией.Таким образом сократите общее количество задач.

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

Стек выделяется до выполнения «main()».Когда вы вызываете функцию b() из функции a(), адрес области хранения сразу после последней переменной, использованной a, передается в b().Это становится началом стека b(), если b() затем вызывает функцию c(), тогда стек c начинается после последней автоматической переменной, определенной b().

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top