Комментарии 85
Так у вас в способе #2 MyHeap каждый раз выделяет блок только с бОльшим адресом?? А что если память кончится? А как возвращать ему память (free()) и переиспользовать её?
В общем виде этот компонент называется "аллокатор памяти" и реализовать его правильно - это грёбаная ядерная физика. В 90% случаев лучше всего взять сторонний а не делать самому.
Память должна распределяться при старте раз и навсегда. Динамическое выделение с потенциальной фрагментацией кучи может привести к отказу из-за неудачного распределения свободных блоков, которые когда-нибудь не соберутся воедино.
От задачи зависит, наверное. Можно легко представить, что embedded-устройством нужно управлять через UI, и в этом случае вряд ли хочется выделять память под все возможные виджеты по всем путям UI, проще зафиксировать какой-то буффер для выделений/освобождений UI-виджетов, можно использовать pool allocator, можно арены/подарены (для embedded достаточно бамп-аллокатор с метками, выйдет, по сути, стек).
См:
https://github.com/SkyEng1neering/dalloc
https://github.com/SkyEng1neering/ustring
https://github.com/SkyEng1neering/uvector
Динамический аллокатор с дефрагментацией памяти при освобождении. И пара stl-like контейнеров на его базе. Как раз для embedded.
конкретно тут как я понял ембед, а в ОС ну типо десктоп, есть сискол(ну или не сискол а стандартизированный вызов на эту операцию точно есть!) стандартизированный, а в другой ОС свой интерфейс шлефанули поверх лоу-левела, тут смотря что обсуждаем я понял что на ембед нету ОС и там своё окружение, а в ос ябы тестил тот вызов и смотрел бы, ведь вроде память самому не взять должен быть интерфейс - он сопственно и предназначен для этого чтобы коммуницировать с чем там ядром наверно
прадедушка ОС это посикс стандарт наверно если по вики смотреть Version 1 Unix перед ним PDP если смотреть в вики на дерево соответственно стандарты там тоже ранжированы и прочее
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, например, либо ещё как-то.
Стоит добавить три важные вещи, о которых все часто забывают:
Если у вас есть кастомный аллокатор, унаследовав его от std::pmr::memory_resource можно его использовать со стандартными контейнерами типа std::pmr::string, std::pmr::vector, и т.д. - и более того, можно с ними использовать разные типы аллокаторов (в одном случае свою кучу, а в другом случае просто локальный буфер), при этом они будут интероперабельны
Если нужно non-monotonic поведение аллокатора (возможность деаллокации), то просто pool allocator (он же arena allocator) на базе списков с фиксированным размером блока в реализации не сильно сложнее чем то, что у автора, зато гораздо более эффективен для многих применений
Существуют реализации
malloc()
иfree()
, которые выполняются за постоянное время и с предсказуемой фрагментацией кучи, что позволяет их использовать даже в системах жесткого реального времени
Когда мы разрабатываем под embedded, нам приходится сталкиваться с такими флагами компиляции как
-nostdlib -fno-exceptions -fno-rtti
избыточность -fno-exceptions -fno-rtti понятна. А чем stdlib мешает?
смею предположить - весом?
насчет размера согласен, и флеш и ram требуются. Но на перфоманс не должно сказываться, в отличии от других флагов из цитаты.
Отсутствие stdlib требует множества нетривиальных действий, и мне кажется что призыв использовать-nostdlib нужно ограничить только особыми случаями, bootloader-ы, OS-и или устройства с мизерной памятью
Современный .NET nano Framework требует от 64кб ОЗУ и умеет динамическую аллокацию
В древности в кнопочных телефонах тоже бывало до 1мб ОЗУ (несколько сотен кб) - но там работала java me с динамической аллокацией
Как апликуха, а тут речь про часть ОС фактически
В драйверах винды тоже куча ограничений
Это который загнулся?
Потому что монструозный и интерпретатор.
Уж тогда лучше брать микропитон
Java me тоже свернули, если что
.NET nanoFramework жив и развивается.
А ещё .NET nanoFramework вроде как меньше ПЗУ требует, меньше ОЗУ требует, быстрее запускается чем MicroPython.
MicroPython тоже интерпретатор.
Получается MicroPython монструозный и интерпретатор - так что не ясно зачем его брать
JavaCard есть, там совсем ничего нет
Объясните мне, а почему стандартный аллокатор отсутствует? Ведь выделять память динамически иногда всё же нужно. С этим ограничением каждый велосипедитчто-то своё, как сказано в данной статье. Небольшой объём памяти - не аргумент. Вполне возможно реализовать простенький аллокатор, работающий на объёмах от единиц до сотен килобайт.
Это из стандартов НАСО на код. Что бы пройти верификатор. От туда же и запрет на рекурсию. А как уж кто извращаться будет…
Контрпродуктивный стандарт какой-то. Мол давайте запретим использовать кучу (дабы чего не вышло), но в результате все используют свои велосипедные замены, что ведёт к возможным ошибкам, которых не было бы, если бы все использовали одну стандартную и посему хорошо протестированную реализацию аллокатора.
Не нужно. Нужно мышление перестроить
Представим, что у нас N независимых задач, каждая из которых может потребовать до M байт памяти. Представим, что физически в системе у нас (N-1)*M памяти.
Что будет, если N-1 задач разом запросят выделения по M байт памяти каждой, а затем N-я задача также запросит память? Аллокатор откажет ей и она должна дождаться, пока кто-нибудь эту память освободит.
Вот это "дождаться" и является недопустимым в задачах реального времени.
Если задачи не-независимы, можно организовать стек и гарантировать, что в момент, когда какой-либо функции потребуется много памяти, этот стек будет пуст.
Если задачи независимы, под каждую придётся зарезервировать тот объём памяти, который потребуется в самом худшем случае.
Динамическое же распределение памяти - это механизм абстрагирования от факта, что физически в системе памяти меньше, чем может потребоваться. В ряде случаев подобное абстрагирование допустимо, но "в embedded" зачастую - нет.
С выделением памяти без кучи стреляют часто себе в ногу, потребляя больше памяти, чем необходимо. Часто ведь под N задач выделяют по M байт памяти на каждую (с запасом), потребляя в результате M*N памяти суммарно. При этом по факту часто каждая задача потребляет меньше, чем M памяти, M - это предел. Так вот, при наличии кучи можно было бы выделять памяти для каждой задачи ровно столько, сколько необходимо, так, что в результате потребление будет меньше, чем M * N, что потенциально может дать возможность обрабатывать больше задач (увеличить N), или же поставить микросхему памяти поменьше.
выделяют по M байт памяти на каждую (с запасом)
Что значит "с запасом"? Задача может потребить M памяти? Если может, если такая вероятность существует и задачи независимы, то системе в целом может потребоваться именно N*M. От этого не уйти никуда.
Если в техзадании говорится, что объём памяти для заказчика критичен, а "подождать" (для задачи) - не критично, следует использовать механизм динамического выделения памяти.
Если в техзадании говорится, что малый объём памяти желателен, а отсутствие "подождать" обязательно, то динамическую аллокацию следует использовать только если вы истово верите в силу вашей молитвы, что случай N*M не произойдёт по мистическим соображениям.
Что значит "с запасом"?
Ну то и значит - количество требуемой памяти может зависеть от задачи, если (например) строки разной длины, или там разное количество объектов, да мало ли. Для совсем простых задач уровня перемножения пары чисел количество памяти обычно фиксировано, но так бывает не всегда.
Если в техзадании говорится, что малый объём памяти желателен, а отсутствие "подождать" обязательно, то динамическую аллокацию следует использовать только если вы истово верите в силу вашей молитвы
Или можно делать динамические аллокации только на старте - один раз распределили, но по умному, а не поровну по M байт на N задач, а дальше уже работаем с выделенным. Если на старте памяти не хватило - мигаем лампочкой, показывая, что что-то пошло не так и оборудование превысило имеющиеся лимиты. Да, чисто технически это не совсем динамическая аллокация, но и не тупое ручное жонглирование блоками памяти.
количество требуемой памяти может зависеть от задачи, если (например) строки разной длины
Вот эту длину вы и фиксируете в техзадании. Хороший пример - ограниченный размер кадра в Ethernet.
Или можно делать динамические аллокации только на старте - один раз распределили, но по умному, а не поровну по M байт на N задач, а дальше уже работаем с выделенным. Если на старте памяти не хватило - мигаем лампочкой, показывая, что что-то пошло не так и оборудование превысило имеющиеся лимиты.
А ещё можно это сделать... на этапе разработки! :) Распределили статически по уму, выделили нужное количество на стек и если не хватило - мигнули лампочкой сообщили заказчику.
Вот эту длину вы и фиксируете в техзадании.
Ограниченный размер != фиксированный размер, это я и пытаюсь донести. Если размер не фиксированный, то иногда можно сэкономить памяти за счёт её более умного распределения.
А ещё можно это сделать... на этапе разработки! :) Распределили статически по уму
По-уму - это руками, с вытекающими из этого ошибками и прострелами ноги? Если вызвать malloc на старте фиксированное количество раз с фиксированными размерами, то результат будет детерминирован, если только malloc не вносит случайности, что в реализации под embedded системы не должно быть. Соответственно, будет достаточно при разработке проверить, что памяти при таком распределении хватило, дальше есть гарантия, что оно везде будет работать как положено.
прострелами ноги
Отраслевая аллегория про прострел ноги относится не ко всем проблемам в IT, а лишь к тем где использование неопытным инженером недокументированного, либо слишком гибкого механизма может привести к неочевидным для него и негативным последствиям.
Единственным последствием отказа от динамической аллокации будет потенциальная недостаточная утилизация памяти. Каких-то неожиданных, непредусмотренных действий подобный код не совершит. Соответственно аналогия с прострелом ноги здесь не к месту.
Поэтому я бы предложил не использовать звучные метафоры, а сосредоточиться на сути обсуждения.
Ограниченный размер != фиксированный размер
Смотрите, текущий размер, конечно может быть меньше выставленного ограничения. Но по своему определению, текущий размер может быть и равен ограничению.
И если вы выделяете в системе несколько полностью независимых процессов с некими ограничениями по занимаемой памяти для каждого процесса, то для гарантированного решения задач реального времени вам придётся исходить из предположения, что все процессы однажды одновременно займут всю память, что была им гарантирована.
Ещё раз. Если вы считаете отдельные процессы действительно независимыми, то у вас нет никакого способа магическим образом "сделать по уму" и уйти от ситуации N*M.
По-уму - это руками, с вытекающими из этого ошибками и прострелами ноги?
Тут проблема экзистенциальна. Зная, что человек склонен ошибаться, может возникнуть наивное желание поручить нечто сложное некой волшебной машине, "которая не ошибается".
Проблема в том, что ситуации, когда "код пишет код" не существует. Существуют ситуации, когда "человек пишет код" и когда "человек пишет код, который пишет код".
То есть в вашем примере распределение памяти всё равно на самом деле будет за человеком. Только при этом вместо того, чтобы выделить массив фиксированной длины, он будет "вызывать malloc фиксированное количество раз". И типа поэтому у человека меньше шансов ошибиться. Как по мне - сомнительный тезис (мягко говоря).
Или можно делать динамические аллокации только на старте - один раз распределили, но по умному, а не поровну по M байт на N задач, а дальше уже работаем с выделенным. Если на старте памяти не хватило - мигаем лампочкой, показывая, что что-то пошло не так и оборудование превысило имеющиеся лимиты.
В основе embeddeb лежит мысль о максимально предсказуемом поведении устройства.
В частности, предсказуемым состоянием должно быть состояние после включения устройства. На этой идее основан, в частности, механизм watchdog - когда небольшая схема внутри системы сознательно и регулярно перезагружает её. И если на условном космическом спутнике под воздействием радиации некая ячейка памяти поменяла своё значение и код вошёл в бесконечный цикл, то ошибочное поведение продлится ровно до очередного сброса системы по сигналу watchdog.
Вы предлагаете на старте системы исходя из неких внутренних и/или внешних состояний по-новому распределять память, а в случае не-успеха мигать лампочкой.
Представляете как это будет работать?
Рабочий включает утром стойку управления станком. Работает до обеда. Выключает стойку. Возвращается, включает её, а стойка мигает лампочкой ))))) Он сообщает мастеру участка, тот звонит в ремонтный цех, ремонтники звонят торговому представителю, те <...> В общем, приезжает инженер фирмы-производителя станка, перезагружает его и всё работает.
Прикольно такой системой реального времени пользоваться, да? :)
Единственным последствием отказа от динамической аллокации будет потенциальная недостаточная утилизация памяти.
Нет. Нужно ещё будет городить свои механизмы выделения памяти, буферы с запасом и т. д.
И если вы выделяете в системе несколько полностью независимых процессов с некими ограничениями по занимаемой памяти для каждого процесса, то для гарантированного решения задач реального времени вам придётся исходить из предположения, что все процессы однажды одновременно займут всю память, что была им гарантирована.
В предлагаемом мною варианте тоже всем задачам в самом худшем случае хватит памяти. Но плюс в том, что если текущие задачи не всю память потребили, может остаться ещё память для дополнительных задач.
поручить нечто сложное некой волшебной машине, "которая не ошибается".
Библиотечный код аллокатора не ошибается, ибо он используется множеством людей и хорошо оттестриован. Свой велосипед в каждом проекте больше подвержен ошибкам, просто в силу меньших усилий, потраченных на его тестирование.
В основе embeddeb лежит мысль о максимально предсказуемом поведении устройства.
Оно и будет предсказуемо. Я написал выше, что нормальная реализация аллокатора должна от запуска к запуску выдавать один и тот же результат.
Рабочий включает утром стойку управления станком. Работает до обеда. Выключает стойку. Возвращается, включает её, а стойка мигает лампочкой
При детерминированной реализации аллокатора такого быть не может. При каждом запуске результат будет тем же. malloc это ведь не генератор случайных чисел, а такой-же код, кем-то написанный с целью быть предсказуемым, только с тем отличаем, что он пишется однократно и хорошо тестируется, чтобы быть использован во множестве проектов.
Все сказки про недетерменированность аллокаций происходят из более сложных систем, где есть виртуальная память, операционная система, множество процессов и т. д. Там да, система может разные куски памяти от запуска к запуску выдавать. Иногда это делается даже намеренно, с целью сделать невозможными некоторые эксплоиты (см. address space randomization). Но в embedded то в основном такого нету и специально-написанный embedded malloc будет работать предсказуемо.
городить свои механизмы выделения памяти
Вы про эти механизмы:
My_type my_array[MY_ARRAY_SIZE];
буферы с запасом
"Буферы с запасом"=недостаточная утилизация памяти. Это единственная потенциальная проблема и её последствия не являются скрытыми.
В предлагаемом мною варианте тоже всем задачам в самом худшем случае хватит памяти. Но плюс в том, что если текущие задачи не всю память потребили, может остаться ещё память для дополнительных задач.
Чтож, представим работу описанной вами системы. В системе N*M байт оперативной памяти. N процессов, решающих задачи реального времени потребляют произвольные, но меньшие предела объёмы памяти. На "свободное" место некий дополнительный процесс, пускай даже не решающий задачи реального времени, пишет некие результаты своих промежуточных вычислений. Внезапно случается N*M. И этому процессу не то, что не выделяется какой-то новый объём памяти, ему другие процессы ультимативно сообщают "на выход!". Причём, так как речь про реальное время, этого самого времени - операций АЛУ - нет на какое-либо подобие архивации или вменяемую приостановку вычислений. Тот процесс вместе со всем насчитанным просто летит в урну. И ждёт пока N*M закончится. А после снова начинает всё сначала в надежде, что успеет отработать пока N*M снова не произойдёт.
Память, гарантированная процессу реального времени может быть и не полностью занята в конкретный момент. Но она может быть полностью истребована в любой момент. И поэтому мне очень любопытен хотя бы один пускай и надуманный, но приближенный к реальности пример, когда вычисления в этом "не занятом" объёме памяти были бы частью какой-то реальной задачи. Что это за задача такая?
При детерминированной реализации аллокатора
Так я говорю не про реализацию, а про использование аллокатора.
Если вы malloc-ом аллоцируете память исходя из каких-то внешних или предшествовавших ресету факторов, то получаете недетерминированное начальное состояние - что как бы капец полнейший.
Если же вы жёстко прописываете операции с malloc, то в чём смысл? Просто создавать массивы и переменные куда легче, к тому же компилятор тогда будет видеть некоторые ошибки, либо угрозы.
Все сказки про недетерменированность аллокаций
Сказок никаких нет. Динамическая аллокация - это попытка заставить систему работать с объёмом памяти, меньшим, чем пиковая потребность в ней, при этом ещё абстрагировавшись от рассмотрения условий возникновения пикового потребления.
Платой за это является зависание части процессов в момент пикового потребления памяти. В embeddeb такое зачастую недопустимо. Поэтому динамическую аллокацию стараются не применять. Поэтому библиотечный malloc в embedded - большая редкость.
И поэтому мне очень любопытен хотя бы один пускай и надуманный, но приближенный к реальности пример, когда вычисления в этом "не занятом" объёме памяти были бы частью какой-то реальной задачи
Тут я пожалуй ответить не смогу. Надо конкретные случаи разбирать. Но теоретическая возможность есть.
Если вы malloc-ом аллоцируете память исходя из каких-то внешних или предшествовавших ресету факторов, то получаете недетерминированное начальное состояние - что как бы капец полнейший.
Я больше скажу. Если внешние факторы меняются, то это уже не детерминизм, даже если malloc не используется. Это тоже капец? Или почему-то капец только malloc каким-то волшебным образом затрагивает, а остального кода не касается?
Сказок никаких нет. Динамическая аллокация - это попытка заставить систему работать с объёмом памяти, меньшим, чем пиковая потребность в ней
Пиковая потребность - теоретическая абстракция. При чём во многих случаях эта теоретическая пиковая потребность околобесконечная, но на практике программа может потреблять очень мало памяти с учётом реальных входных данных.
Платой за это является зависание части процессов
Зависание будет, только если памяти реально недостаточно. В вашем подходе с выделением под каждую задачу максимального количества необходимой памяти программа даже бы не запустилась в таком случае. Но если предположить, что памяти есть в количестве N*M, то динамическое её распределение не приведёт к ситуациям нехватки.
Тут я пожалуй ответить не смогу.
Ну вот то-то и оно.
Теоретически я допускаю, что какая-то крайне экзотическая задача может быть такой.
Но всё что практического приходит в голову заканчивается либо предпросчитанной таблицей значений, либо буфером FIFO/LIFO. В любом случае получается, что без динамической аллокации можно (и нужно) обойтись.
Если внешние факторы меняются, то это уже не детерминизм
Изменение внешних факторов обычно заметно пользователю и/или оговаривается в техзадании в рамках условий эксплуатации.
"Мигание лампочкой" после ресета, происходит на основе логики, скрытой от пользователя.
Плюс, "мигание лампочкой" подразумевает, что переинициализация устройства может закончиться неуспехом, при том, что за секунду до ресета система могла работать штатно.
Или почему-то капец только malloc каким-то волшебным образом затрагивает
Капец затрагивает предложенное вами "мигание лампочкой".
При чём во многих случаях эта теоретическая пиковая потребность околобесконечная
Приведите возможно надуманный, но более-менее реалистичный пример "околобесконечной" пиковой потребности в памяти в задачах embedded, чтобы разговор был предметен.
Но если предположить, что памяти есть в количестве N*M, то динамическое её распределение не приведёт к ситуациям нехватки.
Если память имеется в количестве N*M, то её динамическое распределение является лишней сущностью, в частности, затрудняющей автоматизированную проверку кода различными анализаторами.
То есть да, можно. Но также можно попробовать написать весь код в одну строчку или, например, написать его в стихах.
Можно. Но зачем?
Изменение внешних факторов обычно заметно пользователю и/или оговаривается в техзадании в рамках условий эксплуатации.
Устройство работало в одной конфигурации, конфигурацию поменяли, устройство перезапустили и оно перестало работать. Если вернуть как было, всё будет по-прежнему работать.
"Мигание лампочкой" после ресета, происходит на основе логики, скрытой от пользователя.
Пользователь посмотрит в инструкцию и увидит, что мигание лампочкой три раза означает исчерпание памяти из-за слишком большого количества подключённых периферийных устройств. Дальнейшие действия вполне себе ясны.
Капец затрагивает предложенное вами "мигание лампочкой".
В вашем случае точно такое же мигание будет, если N увеличилось. Только оно будет чаще, ибо выделение памяти с максимальным запасом означает намного большое общее потребление памяти, чем выделение нужного количества через malloc.
Приведите возможно надуманный, но более-менее реалистичный пример "околобесконечной" пиковой потребности в памяти в задачах embedded, чтобы разговор был предметен.
Любой пользовательский ввод данных переменной длины (строк). Вон, выше кто-то писал, что даже UI бывает в embedded запускают.
Если память имеется в количестве N*M, то её динамическое распределение является лишней сущностью
Использование malloc в таком случае - инструмент облегчения написания кода. Программист может использовать привычные ему std::vector и std::string вместо сырых массивов.
Но также можно попробовать написать весь код в одну строчку или, например, написать его в стихах. Можно. Но зачем?
Как раз предлагаемое вами выделение сырых массивов памяти (с запасом) - это усложнение подхода к написанию кода, в сравнении с общепринятым. По сути это переизобретение велосипеда, вместо использование привычных в языке подходов и контейнеров стандартной библиотеки.
Устройство работало в одной конфигурации, конфигурацию поменяли, устройство перезапустили и оно перестало работать.
Кто поменял? Рабочий в цеху механообработки при возвращении с обеда?
Пользователь посмотрит в инструкцию и увидит, что мигание лампочкой три раза означает исчерпание памяти из-за слишком большого количества подключённых периферийных устройств.
А, я понял.
Смотрите, в embedded устройства зачастую не предусматривают их пользовательскую модификацию.
То есть пользователь работает с устройством, как с некой законченной и неизменной сущностью.
Если же требуется модификация устройства, зачастую это происходит так: работа останавливается, приходит инженер техподдержки, добавляет/убавляет/меняет периферийные устройства и накатывает (нередко, путём физической замены карты памяти на плате) новую прошивку на узел обработки.
Но даже если пользователю и дана некоторая свобода модификации устройства путём замены блоков, то сам конструктив устроен так, что в условный крейт CompactPCI вы не поместите модулей больше, чем там есть места. Причём в крейте может быть несколько типов шин - одни более скоростные, другие менее скоростные - и не все слоты равнозначны. Так вот конструктив обычно предусматривает наличие специальных механических ключей, чтобы модули одного типа нельзя было вставить в слоты другого типа.
Разумеется, прошивка вычислительного узла именно что рассчитана на полную загрузку крейта всеми комбинациями модулей, которыми пользователю позволено его загружать.
-При чём во многих случаях эта теоретическая пиковая потребность околобесконечная.
-Приведите возможно надуманный, но более-менее реалистичный пример "околобесконечной" пиковой потребности в памяти в задачах embedded, чтобы разговор был предметен.
-Любой пользовательский ввод данных переменной длины (строк).
Окей, давайте представим, что рабочему требуется загружать программы обработки в G-кодах в станок с ЧПУ.
Это, условно, массив символов.
Допустим, обычно это небольшие программы. Но очень редко требуется исполнить программу объёмом с Войну-и-мир.
Что делать-то будем?
-Скажем рабочему: "знаете, мы тут решили оптимизировать память, поэтому чем больше будет ваша программа обработки, тем хуже будет интерполяция криволинейных перемещений режущего инструмента и, соответственно, качество поверхности детали"?
-Или скрепя сердце выделим под "пользовательский ввод" объём памяти, превышающий все мыслимые пределы и оставим эту память неприкосновенной относительно других применений, при этом сохраним предсказуемость поведения станка вне зависимости от объёмов программы обработки?
Программист может использовать привычные ему std::vector и std::string
Какие конкретно методы std::string, на ваш взгляд, могли бы пригодиться в задачах реального времени и в каких случаях?
Кто поменял? Рабочий в цеху механообработки при возвращении с обеда?
Если ничего не меняется, то не может такого произойти, что устройство работало, но после перезагрузки перестало работать (в той же конфигурации). malloc детерменирован же. Если только специально не вносить в него случайности.
Допустим, обычно это небольшие программы. Но очень редко требуется исполнить программу объёмом с Войну-и-мир.
Что делать-то будем?
Зовём malloc на 100500 символов, получаем nullptr, обрабатываем этот случай и сообщаем пользователю, мол не смогли выделить нужное количество памяти. Со статичным буфером будет точно так же.
Со статичным буфером будет точно так же.
Вот в этом всё и дело.
-Если памяти хватает - её динамическое выделение это избыточная сущность, лишь усложняющая работу.
-Если памяти не хватает, malloc её не родит.
-Не бывает "свободной" памяти, если она гарантирована какому-либо процессу реального времени.
-Если памяти хватает - её динамическое выделение это избыточная сущность, лишь усложняющая работу.
Я с этим в корне не согласен. Динамическое распределение - это не только вызов malloc вручную (так делать не рекомендуется), но и контейнеры стандартной библиотеки. Их использование повышает простоту написания кода. Вы же предлагаете отказаться от преимуществ этих контейнеров и писать в стиле Си - с ручным жонглированием сырыми массивами, что не только усложняет код, но и чревато ошибками, при этом не давая никаких преимуществ. Обосновываете вы это, делая из механизма динамического распределения памяти какое-то пугало, которое не имеет с реальностью ничего общего, и которого якобы стоит избегать.
Динамическое распределение - это не только вызов malloc вручную (так делать не рекомендуется), но и контейнеры стандартной библиотеки. Их использование повышает простоту написания кода...
...в десктопных, мобильных и веб-приложениях приложениях бытового назначения, со свойственными данному назначению ограничениями и возможностями.
Всё так.
Но причём здесь embedded?
Если вызвать malloc на старте фиксированное количество раз с фиксированными размерами, то результат будет детерминирован
<...>
Динамическое распределение - это не только вызов malloc вручную (так делать не рекомендуется), но и контейнеры стандартной библиотеки.
Поправьте если это не так, но у меня создаётся ощущение противоречия. Вы планируете использовать функции стандартных библиотек как чёрный ящик, но при этом полагаете, что сумеете проконтролировать количество вызываемых ими malloc-ов. Верно?
Вы планируете использовать стандартные библиотеки как чёрный ящик, но при этом полагаете, что сумеете проконтролировать количество вызываемых ими malloc-ов. Верно?
Многие контейнеры стандартной библиотеки дают строгие гарантии. Если на старте сделать resize и потом не делать изменений размера, то память перевыделяться не будет. Для std::vector даже изменение размера вниз не приводит к перевыделению памяти, если только не звать shrink_to_fit.
Многие контейнеры стандартной библиотеки дают...
...а немногие не дают.
Суть ваших жалоб в том, что библиотеки созданные для одних предметных областей нельзя применить "из коробки" в других предметных областях.
Развивая данные жалобы/тезисы вы, по сути, спрашиваете "почему все предметные области не могут быть такими же, как те, для которых предназначены обсуждаемые библиотеки".
Ну... так можно и с другой стороны начать жаловаться: почему всем библиотекам std не прикрутили статичность или, условно, стековость "из коробки"? А ещё, желательно, гарантированное время выполнение операций из той же коробки. Ну почемуууу?!
Когда-нибудь может и прикрутят.
Пока придётся для ряда задач в embedded периодически переизобретать некоторые функции, являющиеся стандартными, условно, на десктопах.
При этом, это не означает, что то, как делают на десктопах - правильно, а как в embedded - не правильно. И поэтому в embedded нужно перестать делать неправильно и начать делать правильно.
Это следствие того, что вся электроника, массовая электроника, и за ней - всё IT выросло из персоналок. На персоналках зависание - вариант нормы. Соответственно и для стандартных библиотек - это тоже вариант нормы.
В embedded - это не норма.
Нет никакого смысла пытаться что то объяснить человеку, далёкому от предметной области.
В оригинале там что то про устрицы было
Нет никакого смысла пытаться что то объяснить человеку, далёкому от предметной области.
Ну почему же? :)
Мы уже выяснили, что человеку дорога́ не столько динамическая аллокация, сколько стандартные функции.
Осталось обсудить, гарантируют ли на текущий момент стандартные функции фиксированное/предсказуемое время исполнения (что является основным условием решения задач реального времени) и гештальт вполне может быть закрыт :)
гарантируют ли на текущий момент стандартные функции фиксированное/предсказуемое время исполнения
Надо отдельно смотреть по каждой функции стандартной библиотеки, что там гарантируется. Какой-нибудь push_back, например, в большинстве случаев отрабатывает за константное время, но если нужна реаллокация - за линейное. Плюс много всякой дряни в стандарте есть, которая тянет локаль, из-за чего казалось-бы простой код преобразования целого числа в строку может непредсказуемо медленным быть.
В целом если быть внимательным и не использовать чего-то заведомо тяжеловесное, напороться на непредсказуемое время выполнения будет сложно.
Надо отдельно смотреть по каждой функции стандартной библиотеки
Приведите пример 1-2 стандартных функций, которыми бы вам очень хотелось бы пользоваться, но которые не работают в embedded, так как там не предусмотрена динамическая аллокация. Попробуем вместе заглянуть в стандарт и узнать, есть ли там гарантии времени исполнения?
Ещё недурно было бы определиться с конкретным компилятором: что гарантирует он?
Приведите пример 1-2 стандартных функций, которыми бы вам очень хотелось бы пользоваться, но которые не работают в embedded
std::vector::operator[]. Он работает без проблем. Только чтобы он работал, надо сначала vector::resize сделать, который содержит внутри malloc. resize можно звать при инициализации и там задержка не важна. Зато в ходе работы можно будет использовать operator[] и всё будет предсказуемо без задержек.
Так. А что вас не устраивает в std::array, что вам требуется именно std::vector?
std::vector может быть переменной длины. В нём есть метод size, возвращающий её. В std::array такого нету, там метод size константный, а если фактический размер не равен тому, что выделен при компиляции (меньше него), то надо отдельно хранить текущий размер, что в некоторой степени неудобно и чревато ошибками.
Я думаю, можно подвести некоторый итог.
Полагаю, уместно проговорить несколько связанных вопросов:
1)Почему в embedded практически не используется динамическое выделение памяти?
2)Почему в embedded есть проблемы с использованием стандартных отлаженных библиотек?
3)Как в embedded решать задачи, которые легко реализовались бы при наличие динамического выделения памяти?
1) В бытовых IT-задачах допустимо подвисание отдельных процессов или даже всей системы в целом.
Благодаря этому допущению можно использовать механизм, предоставляющий процессам "бесконечное" количество памяти при её физически ограниченном количестве.
Вы вызываете malloc как чёрный ящик, он "майнит" где-то память, иногда её нужно подождать.
В embedded ждать нельзя. Соответственно такой механизм для embedded не то, чтобы прям запрещён, но он - не актуален. Не актуально так экономить память.
2)Весь бум электроники и бум IT, в первую очередь был сфокусирован на бытовых персональных компьютерах. Соответственно, самые отлаженные библиотеки были написаны именно для бытовых нужд.
Да, под редкие процессорные ядра, работающие в embedded и библиотеки более редкие. Попадается и просто отсутствующий функционал и недостаточно оттестированный функционал.
Это исторически обусловленное явление.
3)Есть ряд задач реального времени отлично ложащийся на механизм динамического выделения памяти, если бы оно имелось.
Например, задача контроля воздушного пространства и учёт летательных аппаратов в воздухе отлично ложится на концепцию динамических списков, которая отлично ложится на динамически выделяемую память.
Но это частная задача.
А так как embedded в целом не столь охвачен программным инструментарием и базово, динамическая аллокация не является для него актуальной, её стандартизированного и отлаженного аналога не существует.
Вы хотите отполированные временем библиотеки для бытовых назначений использовать в embedded. Но они полировались временем не под embedded.
Тот функционал, что полностью совпадает у двух сфер, да, позволит воспользоваться, так сказать, мудростью поколений.
Однако, очевидно, он совпадает не во всём. И простого решения здесь нет.
Нет принципиальной разницы между:
-отказаться от std::vector и реализовать его функционал вручную под свою задачу и
-залить систему оперативной памятью по самую макушку (то есть сделать прямо противоположное, относительно того, зачем динамическая аллокация нужна), взять неизвестно кем написанный и не шибко оттестированный динамический аллокатор и начать гордо использовать std::vector, ведь "он хорошо отлажен и там есть подсчёт элементов".
Как-то так.
Вы хотите отполированные временем библиотеки для бытовых назначений использовать в embedded. Но они полировались временем не под embedded.
Я бы не преувеличивал различия не-embedded и embedded систем до такой степени, чтобы можно было говорить о несовместимости каких-либо базовых библиотек.
С моей точки зрения текущие подходы к embedded разработке обусловлены не спецификой области как таковой, а скорее культурными факторами. Область эта весьма консервативна и разработчики стремятся разрабатывать, как это делали раньше, в том числе в те далёкие времена, когда выделение памяти из кучи было в диковинку.
Насколько я понял, околоиррациональный страх использовать кучу проистекает из ограничений очень древних систем, где памяти было пара килобайт и в задачах, где нужна была микросекундная скорость реакции. Но сейчас даже в очень слабых системах памяти сильно больше, да и задачи со столь жёсткой гарантией времени отклика не доминируют. А там, где гарантированное время отклика всё же нужно, вовсе не обязательно совсем отказываться от использования кучи, надо лишь гарантировать отсутствие динамического перераспределения памяти на критическом пути выполнения.
Показателен тут пример игровой индустрии. Раньше тоже в играх как огня боялись аллокаций из кучи. Но время шло и с ним пришло понимание, что не так страшен чёрт, как его малюют и нужно лишь избегать аллокаций во время игрового процесса (каждый кадр), но вполне нормально использовать кучу во время загрузки игры.
Так вот, разработчики игр всё же изменили свои воззрения, но embedded разработчики всё ещё очень консервативны и придерживаются старых догм. Но, думаю, так будет не вечно и прогресс таки пересилит.
Я бы не преувеличивал различия не-embedded и embedded систем до такой степени, чтобы можно было говорить о несовместимости каких-либо базовых библиотек.
Из личного опыта, у меня однажды в MPLAB IDE, среде разработки под микроконтроллеры PIC, проект на Си отказался компилироваться по причине, что в одной из функций после объявления внутренних переменных шёл код операций с этими переменными, а затем была объявлена ещё одна переменная (ну и далее код продолжался).
То есть особенность паскалеподобных языков, которой нет в Си, когда внутренние переменные функции прописываются в специальном отдельном блоке var в начале функции, внезапно оказалась актуальна для компилятора Си от производителя микроконтроллеров!
Да, всё бывает настолько плохо.
Ваше мнение о близости задач я могу связать с некоторой аберрацией восприятия, характерной для людей с математическим образованием, иронично выраженной в анекдоте про инженера, физика, математика и пожар.
Вы, вероятно полагаете, что если что-то стандартное было написано под x86, то оно с вероятностью 99% также написано и под, условно, C166.
Но задумайтесь:
кто мог написать и написал это нечто под x86 (кто угодно)
и кто бы мог написать это под C166 (затюканный отдел компиляторов в Infineon или вообще парни "быстрей-быстрей" на аутсорсе)?
Показателен тут пример игровой индустрии. Раньше тоже в играх как огня боялись аллокаций из кучи. Но время шло и с ним пришло понимание, что не так страшен чёрт, как его малюют и нужно лишь избегать аллокаций во время игрового процесса (каждый кадр), но вполне нормально использовать кучу во время загрузки игры.
Ровно тоже есть в стандартах НАСА, на сколько я знаю.
Условно, пока ракета стоит Луне - можете использовать динамическую аллокацию, например, для каких-то математических расчётов - задача реального времени не исполняется, динамическая аллокация допустима, чё бы и не да.
Когда дело доходит до старта - приходит время решения задач реального времени и динамическая аллокация становится запрещена.
Есть те устройства, которые всегда решают задачи реального времени. Либо их инициализация не столь обширна, чтобы специально под неё использовать механизм, несовместимый с основной массой рабочей логики.
Соответственно динамическая аллокация в них будет всегда запрещена.
Насколько я понял, околоиррациональный страх использовать кучу проистекает из ограничений очень древних систем, где памяти было пара килобайт...
Во-первых механизм выделения памяти не зависит от объёма этой памяти.
Во-вторых вы понимаете неправильно.
Страха нет, есть понимание, что динамическая аллокация - это, в первую очередь, способ сэкономить память разменяв её на отсутствием гарантий предсказуемого времени исполнения кода.
Подобная экономия, подобный размен не совместимы с задачами реального времени.
Вы же говорите примерно следующее:
-решая задачи реального времени...
-... давайте возьмём механизм экономии памяти...
-...зальём его этой памятью с избытком...
-... и тогда получим возможность использовать библиотеки, которые писались без учёта специфики реального времени...
-...но зато они хорошо отлажены под бытовые нужды и я к ним привык.
С таким подходом вы не будете находить понимания у узкоспециализированых специалистов )))))
что в одной из функций после объявления внутренних переменных шёл код операций с этими переменными, а затем была объявлена ещё одна переменная (ну и далее код продолжался).
это же вроде по стандарту си как раз, по крайней мере по какому древнему правилу языка
это же вроде по стандарту си как раз, по крайней мере по какому древнему правилу языка
Да? Хм. Это как-то странно. Отдельного, явно выраженного блока var нет же.
Есть возможность найти данное положение стандарта? Было бы очень интересно узнать что-то новое :)
Я точно не помню, но посмотрите на чтото старше C99, например C89.
Да, действительно, x86 GCC 1.27 (1988 года) на вот этот код:
int func(void) {
int a=5, b=2, c;
c = a + b;
int d = 7;
c = c - d;
return c;
}
...пишет...
<source>:4: parse error before `int'
<source>:5: undeclared variable `d' (first use here)
Пруф: https://godbolt.org/z/Pq848eGj4
Если перенести int повыше, то всё работает.
Но как бы... MPLAB v8.92 в разделе About указывает, что он 2001-2013.
Мой тезис, что подобное поведение компилятора не соответствует стандарту, конечно ошибочен.
Но мой тезис, что столкновение с подобной версией стандарта в начале 2010-х показывает неспешность разработки инструментария разработчика производителями чипов, ориентированных на embedded, мне кажется в силе.
Зато нечитатели документации по инструментарию сохранились =)
Зато нечитатели документации по инструментарию сохранились =)
...сказал человек, видимо, прочитавший все десять выпусков ISO/IEC9899, каждый из которых примерно по 600 страниц.
Нет, но k&r я читал.
Обычно у меня знакомство с платформой начинается с вопроса "а что умеет компилятор", и какие компиляторы есть
Нет, но k&r я читал.
Обычно у меня знакомство с платформой начинается с вопроса "а что умеет компилятор", и какие компиляторы есть
Ну было бы там написано "full support C95 features", например.
Без чтения стандарта строчка не особо-то информативна.
Подобное поведение с переменными есть и в скриптах си для ЧМИ WinCC Siemens, который далеко не embedded. Видимо по принципу "работает - не трожь". Но можно себя успокоить что не совсем древние стандарты, например не требуется тип аргумента функции указывать отдельно:
void funct1( a, ... )
int a;
{
}
А зачем рисковать тем, что в какой-то момент памяти может не хватить и какой-то процесс не исполниться? Проще поставить железо с запасом. Эмбеддед это вам не персоналка, где какой-то процесс может быть прибит ООМ и перезапущен. В эмбеддед при отказах памяти может быть нехорошо. Память выделят позже, но позже уже может быть не надо, потому что контролируемый объект уже накрылся медным тазом.
Там задачи специфические, обычно список задач определён и большее их количество обрабатывать не надо, а экономия на микросхеме памяти, как видно, может выйти боком.
А зачем рисковать тем, что в какой-то момент памяти может не хватить и какой-то процесс не исполниться? Проще поставить железо с запасом.
Какой-то гипотетический риск. В условной системе со 100 Кб памяти выделить простой реализацией malloc памяти для 100 строк на 20 элементов в среднем будет проще, чем велосипедить свой кривые аллокаторы или выделять под каждую строку памяти с большим запасом. При этом состояние "памяти не хватило" в такой конфигурации не возможно чисто по определению. Не хватить может, только если памяти реально впритык (используется где-то более 50%) и происходят постоянные выделения/освобождения памяти с большим разбросом размеров блоков, что ведёт к сильной фрагментации.
Del - написал взаимоисключающие параграфы
В условной системе со 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];
А теперь представьте, что памяти нужно не под 100 строк по 20 элементов, а под 10 строк на 50 элементов, 20 строк на 80 элементов, 37 структур на 24 элемента, массив из 32 uint32_t элементов и т. д. Уже больше кода будет для всего этого. В моём же предложении будет одна единственная реализация malloc, да и максимальное ограничение размеров не надо будет вручную прописывать.
А теперь представьте, что памяти нужно не под 100 строк по 20 элементов, а под 10 строк на 50 элементов
const int FIRST_MAX_STR_COUNT = 100;
const int FIRST_MAX_STR_LENGTH = 20;
const int SECOND_MAX_STR_COUNT = 10;
const int SECOND_MAX_STR_LENGTH = 50;
int first_strings_processing ()
{
char str_array[FIRST_MAX_STR_COUNT][FIRST_MAX_STR_LENGTH];
<...>
}
int second_strings_processing ()
{
char str_array[SECOND_MAX_STR_COUNT][SECOND_MAX_STR_LENGTH];
<...>
}
int general_strings_processing ()
{
first_strings_processing ();
second_strings_processing ();
<...>
}
Уже больше кода будет для всего этого.
Так его в любом случае будет больше. Вы же явно перечислили 3 различные комбинации количества и размера строк.
Три отдельные, прописанных вами случая - три обработчика. Всё логично.
да и максимальное ограничение размеров не надо будет вручную прописывать.
Прикольно. А что будет делать код процесса, когда он возьмёт памяти больше, чем ему положено? Что будет делать система в целом?
А что будет делать код процесса, когда он возьмёт памяти больше, чем ему положено? Что будет делать система в целом?
Если бы, да кабы. Не будет такого, я выше написал, что есть условные 100 Кб памяти и их исчерпание невозможно для данной конкретной программы в данных конкретных условиях.
Это опять какая-то страшилка вроде бабайки, что дескать malloc может внезапно вернуть nullptr и тогда всё пропало. Такого не происходит, если памяти в системе достаточно, что в embedded гарантировать достаточно просто, оценив требования к количеству памяти заранее (в процессе разработки) и поставив чип памяти соответствующего размера.
есть условные 100 Кб памяти и их исчерпание невозможно
А, тогда ещё проще:
const int MAX_CHAR_BUFF_LENGTH = 100*1024;
char str_array[MAX_CHAR_BUFF_LENGTH];
...и заканчиваем каждую строку символом "\0".
Что вы этим постом хотите доказать? Что по-дурости можно написать программу, требующую количество памяти, больше, чем доступно? Так я не спорю. Я имел в виду случай, когда количество памяти в устройстве выбирается исходя из потребностей программы (+ некий запас). Если вам реально надо памяти под 100Кб строку (с завершающим нулём), то поставьте чип памяти больше 100 Кб.
Отсутствие динамической аллокации в embedded мире
Что вы подразумеваете под "embedded миром"?
Почему динамической аллокации часто нет?
Ограниченные ресурсыПамять (RAM/Flash) в контроллерах может исчисляться десятками или сотнями килобайт, поэтому каждый байт на счету.
...что сравнимо с машинами эпохи Windows 3.11. Там динамическая аллокация была. В общем - не аргумент.
Отсутствие стандартных библиотек
Учитывая, что ядро микроконтроллеров 8051 появилось в 1980 году, у отрасли было 45 лет, чтобы написать стандартные функции аллокации. Я вижу два конкурирующих объяснения, почему этих функций до сих пор нет. Либо в отрасли работают безмозглые идиоты. Либо динамическая аллокация "в embeddeb" не нужна.
Отсутствие библиотек - это как бы следствие, а не причина :)
Встраиваемые приложения часто требуют детерминированного поведения. Фрагментация кучи и непредсказуемые задержки неприемлемы.
Почти так. Только не исключительно фрагментация кучи, а динамическое выделение памяти в общем, по своей сути, является недетерминированным процессом. Поэтому в задачах реального времени, которых в embedded достаточно много, динамическую аллокацию памяти стараются не применять.
Либо в отрасли работают безмозглые идиоты
Это. Часто вообще без прог образования, а только с радиотехнический.
Сразу вспоминается статья Разработчики встраиваемых систем не умеют программировать, первая половина там просто шикарна
Не пробовали Embedded Template Library вместо STL?
Даже по авиационным стандартам можно использовать динамическое выделение памяти, но нельзя память освобождать, т.е. объект, под который дали память должен существовать всё время работы программы. Правда насколько мне известно, вся необходимая память должна быть выделена до взлёта.
В нашем случае программное обеспечение работает под управлением трёхканальной мажоритарной RTOS. Поверх неё расположено системное ПО, реализующее драйверы, протоколы обмена (UDP, RS-485), а также межканальный обмен.
Технологическое ПО взаимодействует с системным уровнем и RTOS через чётко определённые интерфейсы, преимущественно в виде массивов данных (чанков) и callback-функций. Само технологическое ПО напрямую не участвует в сетевом взаимодействии.
Существуют жёсткие архитектурные ограничения:
Отсутствие энергонезависимой памяти — все данные должны храниться и обрабатываться в ОЗУ.
Разделение памяти на секции: • ROM — область только для чтения, где размещаются все объектные модули, классы, статические структуры и т.д. Любые изменения этой области во время выполнения запрещены. (объем его строго не ограничен, оперативной памяти в целом достаточно) • RAM — изменяемая память, которая должна быть минимального объёма. Используется в том числе в межканальном обмене.
Для хранения изменяемых данных используются специальные обёртки — ссылки или константные указатели, указывающие на переменные, размещённые в RAM-секции.
Инициализация выполняется до закрытия ROM-секции контрольными суммами. Это включает:
• Выделение памяти под неизменяемые объекты;
• Вызов конструкторов;
• Полную инициализацию и верификацию (максимально возможную).
После завершения инициализации ROM-секция считается зафиксированной, и любые попытки её изменения приводят к автоматической перезагрузке устройства.
ПО загружается единожды и функционирует весь срок службы устройства без перезапуска. В связи с этим:
• Деструкторы не применяются;
• Аллокация памяти разрешена только на этапе инициализации, в процессе работы динамическое выделение памяти строго запрещено. Поэтому динамически объекты не создаются (разве что на стеке).
соответствующая статья появилась на линкдн, Why I Don't Want the Heap in My Embedded C++: A High-Reliability Perspective
В этой статье очень не хватает раздела про std::pmr
Какое то у вас тут, ребята перекладывание слов из одного кармана в другой в перемешку с фантазиями.
Что мешает использовать например TLSF аллокатор? Можно в связке с stl, pmr или без не важно. Детерминированное время О(1) на все операции, низкая фрагментация.
Отсутствие гарантий.
Аллокатор вернул 0, что делаем?
Фрагментация через 1-2-3 месяца непрерывной работы привела к п. 1. Тот же вопрос.
Извините, у вашего автомобиля недостаточно памяти выполнить процедуру удержания в повороте?
Ну так в статье та же куча предлагается как решение. Разница только в том, какой аллокатор будет эту память делить и отдавать при запросах malloc/free new/delete. Тот же TLSF в стандартной реализации работает с фиксированным буфером который ему дали, где эта память лежит ему наплевать, это может быть стек, куча, GPU или даже файл. Это просто эффективный алгоритм (и один из лучших на данный момент).
Но если вся проблема только в объеме памяти то должны быть известны максимальные размеры всего что может быть выделено. В таком случаях подойдут фиксированных размеров пул аллокторы, монотонные или стек аллокаторы для временных объектов на стеке. Но опять же зависит от объема памяти, если нет очень больших по размеру аллокаций относительно общего объема и на пике будет использовано не более 70-80% от общего объема то TLSF справится. Там каждое освобождение памяти гарантированно склеивает соседние пустые блоки в один большой.
Отсутствие динамической аллокации в embedded мире