Заметка Assembler VS C
Я продолжаю работать над проектом универсального тулкита разработки программ для 8-битных микроконтроллеров.
Моя цель — создать единый ООП-язык (в стиле Java) с унифицированным API и HAL для 8-битных МК (AVR, STM8, PIC).
Хорошо подходящий слоган: «Написано один раз — работает на большинстве МК» (конечно, если это вообще возможно).
Сейчас я разрабатываю методику выделения памяти для примитивов внутри метода и его блоков (условные операторы, циклы и т. п.).
Основные два подхода:
- Динамическое выделение памяти на основе битовой карты блоков памяти.
- Выделение памяти непосредственно в стеке.
Похоже, в итоге я приду к выводу, что первый вариант нужен для хранения полей класса, а второй — для локальных переменных метода.
Судя по всему (подсказка от DeepSeek), всем известный язык C статически выделяет блок в стеке при входе в функцию, при этом:
- выделяет полный объем для хранения всех примитивов (даже тех, которые объявлены в блоках и могут не использоваться);
- не умеет и не позволяет освобождать выделенную память до завершения функции;
- использует дополнительный код для выделения и освобождения блока в стеке.
Теперь я понимаю, почему разработчики на C для МК с малыми ресурсами используют глобальные переменные.
Представьте, сколько памяти потребуется, если в программе будет огромное дерево условий, внутри которого разная логика и разные переменные.
Это действительно огромная трата RAM.
Разрабатывая своё решение, я планирую использовать выделение памяти в стеке только для переменных метода (без учёта блоков), а для каждого блока — отдельную процедуру выделения памяти.
Это позволит существенно экономить RAM.
Но, к сожалению, увеличит размер прошивки, так как для каждого блока потребуется около 16 слов для 16-битного SP и около 5 слов для 8-битного SP (вероятно для 16 бит я вынесу данный код в процедуру, уменя есть подобное решение в core5277).
И это необходимо для высокоуровневого ООП-языка.
Я также планирую добавить опцию в компилятор, позволяющую выбрать, что экономить: RAM или FLASH+CPU.
При выборе FLASH+CPU память будет выделяться статически, как в C.
В
общем, проблема C ясна:
Для каждой функции выделяется блок в стеке под все переменные (даже неиспользуемые), и освобождается он только при завершении функции.
В ассемблере же:
Программист сам решает, как использовать память при входе в функцию.
Часто память вообще не выделяется — многие значения хранятся в регистрах.
То, что не помещается в регистры, можно сохранить одной инструкцией PUSH и восстановить POP.
Можно закрепить ячейку RAM за функцией и использовать её без дополнительных накладных расходов.
Даже
если использовать аналогичные C-подобные макросы, программист, скорее
всего, будет выделять память для конкретных блоков, а не резервировать
максимальный объём для всей функции.
При этом он будет эффективно использовать регистры.
Вот такая огромная разница в оптимизации между ассемблером и C.
К сожалению, я тоже не смогу добиться максимальной оптимизации (как в ассемблере) в своём компиляторе.
НО! В моём тулките это не так критично, как могло бы быть.
Дело в том, что множество низкоуровневых алгоритмов будет реализовано и оптимизировано на ассемблере.
Драйверы также будут на ассемблере и будут использовать эти алгоритмы.
Язык высокого уровня будет их активно применять (часть — прозрачно через кодогенератор компилятора, часть — через явные вызовы).
GitHub проекта: https://github.com/w5277c/vm5277
Визитка проекта: http://vm5277.ru/
P.S. Прошло 3 недели, за две из них я набросал AVR-ассемблер (не полностью, но его функционала и гибкости достаточно для текущих нужд тулкита).
Конечно, я был бы рад использовать avra, но он содержит ошибки, из-за которых не получается собирать мои проекты.
P.P.S. Я сменил лицензию с GPLv3-or-later на Apache-2.0. Судя по всему, это допустимо, так как на данный момент я являюсь автором всего кода в проекте.
Комментарии