Каким был бы лучший способ динамического "изменения" экземпляров относительно динамической памяти?

Поскольку я очень привык к объектно-ориентированному программированию, я хотел бы использовать это в своем дизайне. Мое приложение будет иметь 4 светодиодные полосы, и каждая светодиодная полоса будет иметь "шаблон", работающий на нем. Для этого мне нравится создавать базовый класс и для каждого шаблона производный класс. Таким образом, это означает, что у меня есть 4 экземпляра к шаблону. Однако шаблон может изменяться динамически (на основе музыкального MIDI-входа). Каждый паттерн имеет от 10 до 16 параметров.

Я знаю, что динамическое выделение памяти в основном не является хорошей идеей в Arduino. Я использую Мега, который имеет 8 КБ. Я нашел несколько "решений", но ни в одном из них я не уверен. Первый-это не решение, а то, как я должен это сделать, когда у меня есть "много" SRAM (и упрощено):

class Pattern
{
   public:
      virtual void Process();
}

class PatternA: Pattern
{
   public:
      void Process();

      // В примере я использую только 2 из 10-16 параметров
      uint8_t SetColor(uint8_t colorIndex);
      uint8_t SetSpeed(uint8_t speed);

   private:
      uint8_t _colorIndex;
      uint8_t _speed;
}

// В этом примере я использую только 2 из 24 шаблонов
class PatternB: Pattern
(like PatternA but with its own implementation)

class LedStrip
{
   public:
       void SetPattern(Pattern* pattern);

   private:
       Pattern* _pattern;
}

Другие производные классы могут иметь другие (именованные) параметры.

При смене экземпляров я получал бы что-то вроде этого:

LedStrip::SetPattern(Pattern* pattern)
{
    delete _pattern;
    _pattern = pattern;
}

pattern* pattern = new PatternA(...);
ledStrip1.SetPattern(pattern);

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

Решения:

1. Создайте все возможные шаблоны заранее.

Предположим, я бы реализовал 24 шаблона и создал любую возможную комбинацию, у меня было бы 4 (светодиодные полосы) * 24 (шаблоны) * 13,5 (среднее количество параметров) = 1296 байт. Это кажется немного чересчур, так как только 4 из них "активны".

class LedStrip
{
   public: 
      enum EPattern {A, B}

      void SetPattern(EPattern pattern);

   private:
      PatternA* _patternA;
      PatternB* _patternB;

      Pattern* _pattern;
}

Чтобы изменить шаблон:

void LedStrip::SetPattern(EPatternpattern)
{
case EPatternA: _pattern = _patternA; break;
case EPatternB: _pattern = _patternB; break;
}


ledStrip1.SetPattern(EPattern::A);

Конечно, я мог бы также передать шаблон, где шаблоны создаются глобально, но это не имеет значения для данного примера. Идея состоит в том, чтобы заранее создать 4 экземпляра (светодиодные полосы) * 24 экземпляра (узоры) и никогда не использовать delete. Это означает, что из 4 * 24 паттернов используются только 4, по 1 на светодиодную ленту.

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

2. Создать/Удалить

Я создаю 4 правильных производных экземпляра, и когда один шаблон (экземпляр) изменяется, я удаляю экземпляр и создаю новый. Когда я не помещаю никаких переменных (экземпляров) в производный класс, это может сработать, но у меня есть некоторые конкретные вопросы относительно этого решения:

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

class Pattern
{
   public:
      SetParameter(uint8_t parameter, uint8_t parameterValue);
      uint8_t parameter GetParameter(uint8_t parameter);

   private:
      uint8_t _parameters[16];
}

class PatternA: Pattern
... (does not have any variables)

Чтобы изменить шаблон:

delete ledStrip1.pattern;
ledStrip1.pattern = new PatternA();

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

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

, 👍2

Обсуждение

Комментарии не предназначены для расширенного обсуждения; этот разговор был [перенесен в другое место]. chat](https://chat.stackexchange.com/rooms/120586/discussion-on-question-by-michel-keijzers-what-would-be-the-best-way-of-dynamica)., @Majenko


4 ответа


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

3

Я не знаю, что вы сделаете из этого, но:

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

Размещение new по существу является новым без той части, которая ведет себя как (и, как правило, является) malloc. Или, говоря иначе, он позволяет вам построить объект внутри уже выделенной памяти. Под "уже выделено" я не имею в виду уже выделено в куче; память может прийти откуда угодно. Это более или менее позволяет сделать свой собственный распределитель с предсказуемым поведением, которое вы не получаете от общего распределителя кучи, как malloc.

Таким образом, вы можете выделить память, как вы хотите. Это может быть массив unsigned char в статическом хранилище duration (global, static local, static member). И размещение нового может поместить в него объект или, что важно, его часть. По сути, вы создаете свою собственную арену для своего распределителя.

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

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

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


Что касается избегания new/malloc: дело против них часто немного завышено. Например, выполнение набора malloc / new в начале и только в начале вашей программы, а не после нее, обычно довольно стабильно и предсказуемо. Например, вы можете прочитать конфигурацию из eeprom в setup() и использовать эту информацию для выделения , скажем, трех "динамических" "массивов" с помощью malloc, чтобы вы знали, что их общая сумма не превысит некоторого тестируемого максимума. Возможно, чуть более точное предупреждение для них звучит примерно так: вы попадете в беду, если у вас нет разумного понимания вашего максимального использования кучи и если ваш шаблон использования требует, чтобы вы освободили/удалили/переделали.

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

,

Спасибо за всю эту информацию ... Мне также нужно пройти через него несколько раз (не слышал о новом размещении раньше)., @Michel Keijzers

Если вы когда-либо задавались вопросом, что делает что-то вроде "std::vector", когда вы "изменяете размер ()" до большего, чем текущий размер, " размер ()", ну, в основном это так. Для новых элементов, где вектор был расширен, это размещение new-ing с T - конструктором по умолчанию в память, это в основном ' realloc()`d. Там, где вектор изменяется меньше, он вручную вызывает деструкторы тех элементов, которые уходят., @timemage

Если вы можете *плотно* прибить то, что вы хотите сделать во время выполнения, вы можете обнаружить, что ваш дизайн приближается к чему-то неопределенно похожему на контейнер, только фиксированной емкости (не обязательно размера) и обрабатывает сдерживание двух или трех типов, а не только одного., @timemage

Я знал о перераспределении/сохранении размера для большего/меньшего вектора, но не о новом размещении. Насчет контейнера-это действительно так. Я мог бы сделать его легко выполнимым с помощью большого оператора switch, но я планирую создать больше шаблонов (производных классов), чтобы выбрать из них, и хотел бы сохранить их изолированными (таким образом, используя вывод вместо некоторых переключателей., @Michel Keijzers

Да, я прочитал кое-что в вашем вопросе, что указывало на понимание шаблонов использования кучи. Но это то, что я хотел бы объяснить каждому, кто читает ответ. Я мог бы полностью исключить эту часть, если бы не хотел упомянуть, что резервирование собственной арены через общий распределитель кучи не является неразумным, по моему мнению, в любом случае., @timemage

Что касается "смутно похожего на контейнер", я больше говорил о том, как он управляет своей собственной памятью. Не столько то, как выглядит интерфейс., @timemage

Да, я понимаю ... Я проверю сегодня вечером некоторые идеи, чтобы увидеть, что работает., @Michel Keijzers

Я попробовал новое размещение, и оно работает. Я использую "Max(sizeof(...), sizeof(...), sizeof (...)" для каждого производного типа, поэтому у меня есть только производные классы в одном месте, которое является частью фабрики для создания различных производных экземпляров. Совет для других: не забудьте поставить знак " #include<new>`., @Michel Keijzers

Хорошая сделка. Если я правильно понимаю, то способ, который вы выбрали для этого, примерно похож на то, что я ожидаю увидеть внутри [std::variant](https://en.cppreference.com/w/cpp/utility/variant) делать. Это был именно тот пример, который я думал сделать для вас, но я не был вполне уверен, готовы ли вы принять накладные расходы на калибровку элементов до самого большого типа, и поэтому вам потребовалось бы что-то более сложное, но в остальном использующее размещение new., @timemage

Давайте [продолжим эту дискуссию в чате](https://chat.stackexchange.com/rooms/120600/discussion-between-michel-keijzers-and-timemage)., @Michel Keijzers


1

Классы шаблонов-это код и переменные для текущего набора параметров. Код не принимает SRAM, он выполняется из flash. Я бы использовал все 24 шаблона для всех 4 светодиодных лент, используя глобальный двумерный массив.

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

Наборы параметров могут храниться в PROGMEM или EEPROM и загружаться по мере необходимости в переменные соответствующего экземпляра класса pattern.

,

Что касается идеи расширенной версии, я сомневаюсь, что это будет работать честно. В худшем случае мне нужно 4 экземпляра каждого (класса pattern), которые работают без памяти (или используют ее много). Возможно, это было не ясно, но параметры гибки, что означает, что у меня может быть 20 экземпляров класса pattern для одного шаблона со всеми различными параметрами. Наборы параметров могут храниться в EEPROM/PROGMEM, но параметры должны быть скопированы в память (до четырех экземпляров). Но, может быть, мой вопрос не совсем ясен., @Michel Keijzers

@MichelKeijzers, не можете ли вы загрузить параметры в существующий неиспользуемый экземпляр класса pattern? как я понимаю, есть только 4 активных паттерна. таким образом, в худшем случае пул будет содержать 4 x 24 объекта шаблона, @Juraj

Да, но это уже слишком ... 4 * 24 паттерна * 16 (параметры) = 1,536 байта, что довольно много. Тем более, что будут нужны и другие значения., @Michel Keijzers


1

Когда мне нужно динамически выделять память, я обычно предварительно выделяю пул блоков памяти, каждый достаточно большой для запроса максимального размера, который я буду обрабатывать. Затем, независимо от того, запрашивает ли приложение 1 байт или MAXSIZE байт, оно получает указатель на первый доступный один из этих блоков MAXSIZE в пуле. free()ing такой блок работает так, как вы ожидаете. Поскольку все выделения имеют одинаковый размер, и из реальной кучи никогда не берется больше памяти, пул не должен (и не может, в моей реализации) расти.

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

,

Спасибо за ответ, я использовал placement new, который отлично работает в моем случае. В моем случае у меня есть только 4 фиксированных элемента (которые теперь будут все время "обновляться" и разрушаться, но не удаляться)., @Michel Keijzers


1

Поскольку вам нужно только четыре объекта в любой момент времени, было бы расточительно статически выделять 4×24 объекта, так как вам действительно нужно только в четыре раза больше самого большого объекта. Вы все еще можете использовать статическое распределение с помощью массива объединений: каждая ячейка массива будет достаточно большой , чтобы хранить максимально возможный шаблон.

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

Ниже приведен упрощенный пример с двумя классами, каждый из которых содержит только число, либо uint8_t, либо uint16_t:

class Pattern
{
public:
    virtual void process();
};

class Pattern8 : public Pattern
{
public:
    void process() { Serial.println(8000 + param); }
    void set_param(uint8_t param8) { param = param8; }
private:
    uint8_t param;
};

class Pattern16 : public Pattern
{
public:
    void process() { Serial.println(16000 + param); }
    void set_param(uint8_t param16) { param = param16; }
private:
    uint16_t param;
};

// Это помеченный союз.
struct AnyPattern : public Pattern
{
    AnyPattern() {}  // должен быть явно определен
    void process() {
        switch (type) {
            case TYPE8: pattern8.process(); break;
            case TYPE16: pattern16.process(); break;
        }
    }
    enum { TYPE8, TYPE16 } type;
    union {
        Pattern8 pattern8;
        Pattern16 pattern16;
    };
};

void setup() {
    AnyPattern patterns[2];
    patterns[0].type = AnyPattern::TYPE8;
    patterns[0].pattern8.set_param(21);
    patterns[1].type = AnyPattern::TYPE16;
    patterns[1].pattern16.set_param(42);
    Serial.begin(9600);
    patterns[0].process();
    patterns[1].process();
}

void loop(){}
,

Я поддержал его, хотя, честно говоря, мне не очень нравится решение "союза", хотя оно и работает. Я использовал метод "размещения нового", который отлично работает для меня., @Michel Keijzers