Когда мы разрабатываем под embedded, нам приходится сталкиваться с такими флагами компиляции как -nostdlib -fno-exceptions -fno-rtti.

Во многих средах нет malloc/free (new/delete) и вообще нет встроенного выделения памяти.

Использование «больших» стандартных контейнеров C++ (например, std::vector) нередко исключено

В результате приходится решать задачу «ручного» управления памятью. Ниже рассмотрим два подхода:

  • Буфер + переопределение оператора new (placement new)

  • Собственная куча (Heap)

    Почему динамической аллокации часто нет

    1. Ограниченные ресурсы

      Память (RAM/Flash) в контроллерах может исчисляться десятками или сотнями килобайт, поэтому каждый байт на счету.

    2. Отсутствие стандартных библиотек

      При флагах -nostdlib и -fno-exceptions мы исключаем всю поддержку C/C++ runtime:

      • Нет new/delete (кроме placement-new, если его оставить).

      • malloc/free нередко не реализованы или ведут к непредсказуемым последствиям (heap overflow, фрагментация).

    3. Простота сертификации и детерминированность
      Встраиваемые приложения часто требуют детерминированного поведения. Фрагментация кучи и непредсказуемые задержки неприемлемы.

    Поэтому разработчики вынуждены либо заранее выделять «сырой» буфер и вызывать placement new, либо писать собственный аллокатор (кучу) на фиксированном участке памяти.

    Способ 1. Буфер + placement new

    Идея

    • Заранее выделить в статической памяти фиксированный буфер (массив байт), подходящий по размеру и выравниванию под нужный объект.

    • При необходимости «конструировать» объект поверх этого буфера с помощью placement new.

    • При этом не использовать глобальный operator new(size_t), а локально в коде писать new (&buffer) Type(args...).

      Пример

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

// cgf.h
#pragma once

namespace PpoDbTimer {
	enum PpoDbTimerEnum : int{
		timer1,
        TotalCount
    };
}
// ram.cpp
#include "cfg.h"

namespace PpoDbRam {
    int timers     [ PpoDbTimer::TotalCount     ] { 0 };
}
// rom.cpp
#include <cstddef>
#include "cfg.h"

namespace PpoDbRam {
    extern int timers[];
}

namespace Ppo {
    class timer{
        public:
            explicit timer(int *_timer) noexcept :time(_timer) {}

            void tick() noexcept {
                if (time) {
                    ++(*time);
                }
            }
                    
        private:
            int * const time;
            /// ...
    };
}

/*
    Переопределение оператора new
*/
inline void *operator new( size_t, void *ptr ) { return ptr; }

namespace {
    template <size_t Len, size_t Align>
    struct aligned_storage{
        struct type{
            alignas(Align) unsigned char data[Len];
        };
    };

    aligned_storage< sizeof( Ppo::timer ), alignof( Ppo::timer ) >::type timer1Buf;
}

void init(){
   auto & timer1 = *new (&timer1Buf) Ppo::timer(&PpoDbRam::timers[PpoDbTimer::timer1]);
   timer1.tick();
}


int main(){
    init();
    return 0;
}

Что важно

  • Статический буфер timer1Buf хранится в области .bss или .data (в зависимости от инициализации).

  • Выравнивание: используется std::aligned_storage_t<…>, чтобы гарантировать alignof(Ppo::timer).

  • Placement new: new (raw) Ppo::timer(timerAddr) не вызывает глобальный operator new(size_t), а просто конструирует объект в уже выделенной памяти.

  • Отсутствие delete: поскольку у нас нет поддержки освобождения, объект живёт «вечно» до конца программы.

  • UB при обращении до init(): если где-то код попытается прочитать timer1Ptr->… до вызова init(), будет UB.

Способ 2. Собственная куча (Heap)

Идея

  1. Создать класс MyHeap, который внутри держит:

    • Статический массив-буфер (например, 2 МБ)

    • Текущий сдвиг (offset) — сколько байт уже занято.

  2. Реализовать метод allocate(size_t size, size_t alignment):

    • Выравнивять текущий указатель вверх под alignment.

    • Проверить, что offset + size ≤ BUFFER_SIZE.

    • При успехе вернуть void* на выровненный адрес и обновить offset.

    • При нехватке памяти вернуть nullptr.

  3. (Опционально) Переопределить глобальный operator new(size_t) так, чтобы он делал MyHeap::allocate(...). И тогда в коде можно писать new T(args...), и вся память будет «брать» из нашего буфера. Но чаще держат create<T>(...) внутри MyHeap, чтобы не мешать стандартным new в других модулях.

Пример

//heap.h
#pragma once

#include <cstddef>

class MyHeap {
public:
    // Пытается выдать size байт с выравниванием alignment. Возвращает nullptr, если кончилась память.
    static void* allocate(size_t size, size_t alignment = alignof(size_t)) noexcept {
        size_t cur = reinterpret_cast<size_t>(buffer) + offset;
        size_t aligned = (cur + alignment - 1) & ~(alignment - 1);
        size_t newOffset = static_cast<size_t>(aligned - reinterpret_cast<size_t>(buffer)) + size;

        if (newOffset > BUFFER_SIZE)
            return nullptr;

        void* ptr = reinterpret_cast<void*>(aligned);
        offset = newOffset;
        return ptr;
    }

    template <typename T, typename... Args>
    static T* create(Args&&... args) noexcept {
        void* mem = allocate(sizeof(T), alignof(T));
        if (!mem) 
            return nullptr;
        return new (mem) T(static_cast<Args&&>(args)...);
    }
private:
    static constexpr size_t BUFFER_SIZE = 2 * 1024 * 1024;
    alignas(alignof(size_t)) static char buffer[];
    static size_t offset;
};
// rom.cpp 
#include "heap.h"
#include "cfg.h"
#include <cstddef>

namespace PpoDbRam {
    extern int timers[];
}

alignas(alignof(std::max_align_t)) char MyHeap::buffer[ MyHeap::BUFFER_SIZE ];
std::size_t MyHeap::offset = 0;

// Оставляем стандартный «placement new», чтобы new(mem) T() работал:
void* operator new(size_t, void* ptr) noexcept {
    return ptr;
}

// Глобальный operator new(size) будет выделять через MyHeap::allocate.
void* operator new(size_t size) noexcept {
    return MyHeap::allocate(size, alignof(max_align_t));
}

// Пустой operator delete (никакого освобождения)
void operator delete(void*) noexcept {
}

void operator delete(void*, size_t) noexcept {
}

namespace Ppo {
    class timer{
        public:
            explicit timer(int *_timer) noexcept :time(_timer) {}

            void tick() noexcept {
                if (time) {
                    ++(*time);
                }
            }
                    
        private:
            int * const time;
            /// ...
    };
}

void init(){
    auto & timer1 = *new Ppo::timer(&PpoDbRam::timers[ PpoDbTimer::timer1 ]);
    timer1.tick();
}

int main(){
    init();
    return 0;
}

Сравнение двух подходов

Критерий

Способ 1 (буфер + placement new)

Способ 2 (своя куча)

Простота реализации

Очень просто: для каждого объекта — свой буфер и placement new.

Нужно написать класс-кучу, методы allocate/offset, переопределить new/delete (если требуется).

Гибкость

Подходит, когда заранее известен тип и кол-во объектов.

Позволяет произвольно вызывать new T() десятки и сотни раз до исчерпания буфера.

Утилизация памяти

«Жестко» закреплён за конкретным объектом.

Можно выделять разные размеры, кучу «жирнее» (несколько объектов в одном буфере, при одинаковом выравнивании).

Разрез «тайминг»

Инициализация только в том месте, где объявлен буфер.

Выделение памяти в любом месте программы, пока осталось место.

Кодовая избыточность

При N разных типов/объектов — N буферов.

Один класс-куча для всех типов.

Риск UB и ошибок

Мало кода — меньше шансов допустить ошибку.

Больше кода/логики — может быть ошибка в вычислении выравнивания, offset и т. д.

Рекомендации

  • Если вам нужно одно-два статических объекта фиксированных типов, проще использовать Способ 1:

static std::aligned_storage_t<sizeof(MyType), alignof(MyType)> myObjBuf;
MyType& myObj = *new (&myObjBuf) MyType(arg…);

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

  • Если вы пишете более сложное приложение, где создаются десятки (или тысячи как в моем проекте) объектов разных типов, выбирайте Способ 2 и реализуйте свою кучу. Так вы сможете писать обычные new T(), а все они будут «упаковываться» в предопределённый буфер. Особенно если Вам надо саллоцировать что-то такое unordered_map<int,array<int>>.

Заключение

В embedded-мире почти всегда отсутствует стандартный allocator и вся стихийная флеш-рамка стандартного C++ отключена флагами -nostdlib -fno-exceptions -fno-rtti. Если задача требует динамического (пусть и внутри жестко ограниченного) выделения памяти, у вас есть два основных пути:

  1. Буфер+placement new — быстрый и надёжный способ, когда заранее известны объёмы и типы.

  2. Собственная куча — гибкий и масштабируемый способ, когда объём и количество объектов заранее не фиксированы.

Комментарии (36)


  1. Cheater
    13.06.2025 11:34

    Так у вас в способе #2 MyHeap каждый раз выделяет блок только с бОльшим адресом?? А что если память кончится? А как возвращать ему память (free()) и переиспользовать её?

    В общем виде этот компонент называется "аллокатор памяти" и реализовать его правильно - это грёбаная ядерная физика. В 90% случаев лучше всего взять сторонний а не делать самому.


    1. a-tk
      13.06.2025 11:34

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


      1. Vladislav_Dudnikov
        13.06.2025 11:34

        От задачи зависит, наверное. Можно легко представить, что embedded-устройством нужно управлять через UI, и в этом случае вряд ли хочется выделять память под все возможные виджеты по всем путям UI, проще зафиксировать какой-то буффер для выделений/освобождений UI-виджетов, можно использовать pool allocator, можно арены/подарены (для embedded достаточно бамп-аллокатор с метками, выйдет, по сути, стек).


      1. skyengine
        13.06.2025 11:34

        См:

        https://github.com/SkyEng1neering/dalloc

        https://github.com/SkyEng1neering/ustring

        https://github.com/SkyEng1neering/uvector

        Динамический аллокатор с дефрагментацией памяти при освобождении. И пара stl-like контейнеров на его базе. Как раз для embedded.


    1. Jijiki
      13.06.2025 11:34

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

      прадедушка ОС это посикс стандарт наверно если по вики смотреть Version 1 Unix перед ним PDP если смотреть в вики на дерево соответственно стандарты там тоже ранжированы и прочее


  1. a-tk
    13.06.2025 11:34

    template<class T, int Id = 0>
    T& instance()
    {
      static char data[sizeof(T)];
      static bool initialized;
    
      if (!initialized)
      {
        initialized = true;       
        new(data)T();
      }
      return *reinterpret_cast<T*>(data);
    }
    
    instance<Timer>();
    instance<Channel, 0>();
    instance<Channel, 1>();

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

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


  1. Uporoty
    13.06.2025 11:34

    Стоит добавить три важные вещи, о которых все часто забывают:

    1. Если у вас есть кастомный аллокатор, унаследовав его от std::pmr::memory_resource можно его использовать со стандартными контейнерами типа std::pmr::string, std::pmr::vector, и т.д. - и более того, можно с ними использовать разные типы аллокаторов (в одном случае свою кучу, а в другом случае просто локальный буфер), при этом они будут интероперабельны

    2. Если нужно non-monotonic поведение аллокатора (возможность деаллокации), то просто pool allocator (он же arena allocator) на базе списков с фиксированным размером блока в реализации не сильно сложнее чем то, что у автора, зато гораздо более эффективен для многих применений

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


  1. viordash
    13.06.2025 11:34

    Когда мы разрабатываем под embedded, нам приходится сталкиваться с такими флагами компиляции как -nostdlib -fno-exceptions -fno-rtti

    избыточность -fno-exceptions -fno-rtti понятна. А чем stdlib мешает?


    1. ippishio
      13.06.2025 11:34

      смею предположить - весом?


  1. maxcat
    13.06.2025 11:34

    Современный .NET nano Framework требует от 64кб ОЗУ и умеет динамическую аллокацию

    В древности в кнопочных телефонах тоже бывало до 1мб ОЗУ (несколько сотен кб) - но там работала java me с динамической аллокацией


    1. Kealon
      13.06.2025 11:34

      Как апликуха, а тут речь про часть ОС фактически

      В драйверах винды тоже куча ограничений


      1. maxcat
        13.06.2025 11:34

        .NET nano Framework запускается без ос


    1. Siemargl
      13.06.2025 11:34

      Это который загнулся?

      Потому что монструозный и интерпретатор.

      Уж тогда лучше брать микропитон

      Java me тоже свернули, если что


      1. maxcat
        13.06.2025 11:34

        .NET nanoFramework жив и развивается.

        А ещё .NET nanoFramework вроде как меньше ПЗУ требует, меньше ОЗУ требует, быстрее запускается чем MicroPython.

        MicroPython тоже интерпретатор.

        Получается MicroPython монструозный и интерпретатор - так что не ясно зачем его брать


      1. Xadok
        13.06.2025 11:34

        JavaCard есть, там совсем ничего нет


  1. Panzerschrek
    13.06.2025 11:34

    Объясните мне, а почему стандартный аллокатор отсутствует? Ведь выделять память динамически иногда всё же нужно. С этим ограничением каждый велосипедитчто-то своё, как сказано в данной статье. Небольшой объём памяти - не аргумент. Вполне возможно реализовать простенький аллокатор, работающий на объёмах от единиц до сотен килобайт.


    1. Kealon
      13.06.2025 11:34

      Это из стандартов НАСО на код. Что бы пройти верификатор. От туда же и запрет на рекурсию. А как уж кто извращаться будет…


      1. Panzerschrek
        13.06.2025 11:34

        Контрпродуктивный стандарт какой-то. Мол давайте запретим использовать кучу (дабы чего не вышло), но в результате все используют свои велосипедные замены, что ведёт к возможным ошибкам, которых не было бы, если бы все использовали одну стандартную и посему хорошо протестированную реализацию аллокатора.


    1. Siemargl
      13.06.2025 11:34

      Не нужно. Нужно мышление перестроить


    1. Flammmable
      13.06.2025 11:34

      Представим, что у нас N независимых задач, каждая из которых может потребовать до M байт памяти. Представим, что физически в системе у нас (N-1)*M памяти.

      Что будет, если N-1 задач разом запросят выделения по M байт памяти каждой, а затем N-я задача также запросит память? Аллокатор откажет ей и она должна дождаться, пока кто-нибудь эту память освободит.

      Вот это "дождаться" и является недопустимым в задачах реального времени.

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

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

      Динамическое же распределение памяти - это механизм абстрагирования от факта, что физически в системе памяти меньше, чем может потребоваться. В ряде случаев подобное абстрагирование допустимо, но "в embedded" зачастую - нет.


      1. Panzerschrek
        13.06.2025 11:34

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


        1. Flammmable
          13.06.2025 11:34

          выделяют по M байт памяти на каждую (с запасом)

          Что значит "с запасом"? Задача может потребить M памяти? Если может, если такая вероятность существует и задачи независимы, то системе в целом может потребоваться именно N*M. От этого не уйти никуда.

          Если в техзадании говорится, что объём памяти для заказчика критичен, а "подождать" (для задачи) - не критично, следует использовать механизм динамического выделения памяти.

          Если в техзадании говорится, что малый объём памяти желателен, а отсутствие "подождать" обязательно, то динамическую аллокацию следует использовать только если вы истово верите в силу вашей молитвы, что случай N*M не произойдёт по мистическим соображениям.


          1. Panzerschrek
            13.06.2025 11:34

            Что значит "с запасом"?

            Ну то и значит - количество требуемой памяти может зависеть от задачи, если (например) строки разной длины, или там разное количество объектов, да мало ли. Для совсем простых задач уровня перемножения пары чисел количество памяти обычно фиксировано, но так бывает не всегда.

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

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


            1. Flammmable
              13.06.2025 11:34

              количество требуемой памяти может зависеть от задачи, если (например) строки разной длины

              Вот эту длину вы и фиксируете в техзадании. Хороший пример - ограниченный размер кадра в Ethernet.

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

              А ещё можно это сделать... на этапе разработки! :) Распределили статически по уму, выделили нужное количество на стек и если не хватило - мигнули лампочкой сообщили заказчику.


              1. Panzerschrek
                13.06.2025 11:34

                Вот эту длину вы и фиксируете в техзадании.

                Ограниченный размер != фиксированный размер, это я и пытаюсь донести. Если размер не фиксированный, то иногда можно сэкономить памяти за счёт её более умного распределения.

                А ещё можно это сделать... на этапе разработки! :) Распределили статически по уму

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


                1. Flammmable
                  13.06.2025 11:34

                  прострелами ноги

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

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

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

                  Ограниченный размер != фиксированный размер

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

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

                  Ещё раз. Если вы считаете отдельные процессы действительно независимыми, то у вас нет никакого способа магическим образом "сделать по уму" и уйти от ситуации N*M.

                  По-уму - это руками, с вытекающими из этого ошибками и прострелами ноги?

                  Тут проблема экзистенциальна. Зная, что человек склонен ошибаться, может возникнуть наивное желание поручить нечто сложное некой волшебной машине, "которая не ошибается".

                  Проблема в том, что ситуации, когда "код пишет код" не существует. Существуют ситуации, когда "человек пишет код" и когда "человек пишет код, который пишет код".

                  То есть в вашем примере распределение памяти всё равно на самом деле будет за человеком. Только при этом вместо того, чтобы выделить массив фиксированной длины, он будет "вызывать malloc фиксированное количество раз". И типа поэтому у человека меньше шансов ошибиться. Как по мне - сомнительный тезис (мягко говоря).

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

                  В основе embeddeb лежит мысль о максимально предсказуемом поведении устройства.

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

                  Вы предлагаете на старте системы исходя из неких внутренних и/или внешних состояний по-новому распределять память, а в случае не-успеха мигать лампочкой.

                  Представляете как это будет работать?

                  Рабочий включает утром стойку управления станком. Работает до обеда. Выключает стойку. Возвращается, включает её, а стойка мигает лампочкой ))))) Он сообщает мастеру участка, тот звонит в ремонтный цех, ремонтники звонят торговому представителю, те <...> В общем, приезжает инженер фирмы-производителя станка, перезагружает его и всё работает.

                  Прикольно такой системой реального времени пользоваться, да? :)


                  1. Panzerschrek
                    13.06.2025 11:34

                    Единственным последствием отказа от динамической аллокации будет потенциальная недостаточная утилизация памяти.

                    Нет. Нужно ещё будет городить свои механизмы выделения памяти, буферы с запасом и т. д.

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

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

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

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

                    В основе embeddeb лежит мысль о максимально предсказуемом поведении устройства.

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

                    Рабочий включает утром стойку управления станком. Работает до обеда. Выключает стойку. Возвращается, включает её, а стойка мигает лампочкой

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

                    Все сказки про недетерменированность аллокаций происходят из более сложных систем, где есть виртуальная память, операционная система, множество процессов и т. д. Там да, система может разные куски памяти от запуска к запуску выдавать. Иногда это делается даже намеренно, с целью сделать невозможными некоторые эксплоиты (см. address space randomization). Но в embedded то в основном такого нету и специально-написанный embedded malloc будет работать предсказуемо.


                    1. Flammmable
                      13.06.2025 11:34

                      городить свои механизмы выделения памяти

                      Вы про эти механизмы:

                      My_type my_array[MY_ARRAY_SIZE];

                      буферы с запасом

                      "Буферы с запасом"=недостаточная утилизация памяти. Это единственная потенциальная проблема и её последствия не являются скрытыми.

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

                      Чтож, представим работу описанной вами системы. В системе N*M байт оперативной памяти. N процессов, решающих задачи реального времени потребляют произвольные, но меньшие предела объёмы памяти. На "свободное" место некий дополнительный процесс, пускай даже не решающий задачи реального времени, пишет некие результаты своих промежуточных вычислений. Внезапно случается N*M. И этому процессу не то, что не выделяется какой-то новый объём памяти, ему другие процессы ультимативно сообщают "на выход!". Причём, так как речь про реальное время, этого самого времени - операций АЛУ - нет на какое-либо подобие архивации или вменяемую приостановку вычислений. Тот процесс вместе со всем насчитанным просто летит в урну. И ждёт пока N*M закончится. А после снова начинает всё сначала в надежде, что успеет отработать пока N*M снова не произойдёт.

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

                      При детерминированной реализации аллокатора

                      Так я говорю не про реализацию, а про использование аллокатора.

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

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

                      Все сказки про недетерменированность аллокаций

                      Сказок никаких нет. Динамическая аллокация - это попытка заставить систему работать с объёмом памяти, меньшим, чем пиковая потребность в ней, при этом ещё абстрагировавшись от рассмотрения условий возникновения пикового потребления.

                      Платой за это является зависание части процессов в момент пикового потребления памяти. В embeddeb такое зачастую недопустимо. Поэтому динамическую аллокацию стараются не применять. Поэтому библиотечный malloc в embedded - большая редкость.


        1. me21
          13.06.2025 11:34

          А зачем рисковать тем, что в какой-то момент памяти может не хватить и какой-то процесс не исполниться? Проще поставить железо с запасом. Эмбеддед это вам не персоналка, где какой-то процесс может быть прибит ООМ и перезапущен. В эмбеддед при отказах памяти может быть нехорошо. Память выделят позже, но позже уже может быть не надо, потому что контролируемый объект уже накрылся медным тазом.

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


          1. Panzerschrek
            13.06.2025 11:34

            А зачем рисковать тем, что в какой-то момент памяти может не хватить и какой-то процесс не исполниться? Проще поставить железо с запасом.

            Какой-то гипотетический риск. В условной системе со 100 Кб памяти выделить простой реализацией malloc памяти для 100 строк на 20 элементов в среднем будет проще, чем велосипедить свой кривые аллокаторы или выделять под каждую строку памяти с большим запасом. При этом состояние "памяти не хватило" в такой конфигурации не возможно чисто по определению. Не хватить может, только если памяти реально впритык (используется где-то более 50%) и происходят постоянные выделения/освобождения памяти с большим разбросом размеров блоков, что ведёт к сильной фрагментации.


            1. me21
              13.06.2025 11:34

              Del - написал взаимоисключающие параграфы


            1. Flammmable
              13.06.2025 11:34

              В условной системе со 100 Кб памяти выделить простой реализацией malloc памяти для 100 строк на 20 элементов в среднем будет проще, чем велосипедить свой кривые аллокаторы или выделять под каждую строку памяти с большим запасом.

              const int MAX_STR_COUNT = 100;
              const int MAX_STR_LENGTH = 20;
              char str_array[MAX_STR_COUNT][MAX_STR_LENGTH];


  1. Flammmable
    13.06.2025 11:34

    Отсутствие динамической аллокации в embedded мире

    Что вы подразумеваете под "embedded миром"?

    Почему динамической аллокации часто нет?
    Ограниченные ресурсы

    Память (RAM/Flash) в контроллерах может исчисляться десятками или сотнями килобайт, поэтому каждый байт на счету.

    ...что сравнимо с машинами эпохи Windows 3.11. Там динамическая аллокация была. В общем - не аргумент.

    Отсутствие стандартных библиотек

    Учитывая, что ядро микроконтроллеров 8051 появилось в 1980 году, у отрасли было 45 лет, чтобы написать стандартные функции аллокации. Я вижу два конкурирующих объяснения, почему этих функций до сих пор нет. Либо в отрасли работают безмозглые идиоты. Либо динамическая аллокация "в embeddeb" не нужна.

    Отсутствие библиотек - это как бы следствие, а не причина :)

    Встраиваемые приложения часто требуют детерминированного поведения. Фрагментация кучи и непредсказуемые задержки неприемлемы.

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


    1. Gordon01
      13.06.2025 11:34

      Либо в отрасли работают безмозглые идиоты

      Это. Часто вообще без прог образования, а только с радиотехнический.


      1. Uporoty
        13.06.2025 11:34

        Сразу вспоминается статья Разработчики встраиваемых систем не умеют программировать, первая половина там просто шикарна


  1. AbitLogic
    13.06.2025 11:34

    Не пробовали Embedded Template Library вместо STL?