C++ на встраиваемых целях

Я нахожусь в процессе кодирования многоразового модуля C++ для процессора ARM Cortex-M4. Модуль использует много памяти для выполнения своей задачи, и это критично ко времени.

Чтобы позволить пользователям моего модуля настраивать его поведение, я использую разные backend-классы для разных реализаций задач низкого уровня. Одним из таких бэкэндов является бэкэнд-хранилище, предназначенный для хранения фактических данных в различных типах энергозависимой / энергонезависимой оперативной памяти. Он состоит в основном из функций set/get, которые очень быстро выполняются и будут вызываться очень часто. Они в основном в такой форме:

uint8_t StorageBackend::getValueFromTable(int row, int column, int parameterID) 
{
    return table[row][column].parameters[parameterID];
}

uint8_t StorageBackend::getNumParameters() { return kNumParameters; }

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

Теперь мне интересно, какой шаблон проектирования выбрать для отделения аспектов хранения от моего основного модуля.

  1. Класс с виртуальными функциями был бы простым, но мощным вариантом. Тем не менее, я опасаюсь, что стоимость вызова виртуальных функций set/get очень часто возникает в критической по времени среде. Особенно для хранилища данных это может быть серьезной проблемой.
  2. Обеспечение основного класса модулей параметрами шаблона для его различных бэкэндов (может быть, даже с CRTP-шаблоном?). Это позволит избежать виртуальных функций и даже позволит встроить функции set/get в серверную часть хранилища. Однако, это потребовало бы, чтобы весь основной класс был реализован в заголовочном файле, который не особенно аккуратен...
  3. Используйте простые функции в стиле C, чтобы сформировать бэкэнд хранилища.
  4. Используйте макросы для простых функций set/get (после компиляции это должно быть примерно так же, как в варианте 2 со всеми встроенными функциями set/get.)
  5. Определите структуры хранения данных самостоятельно и разрешите настройку, используя макросы в качестве типов данных. Например RAM_UINT8 table[ROWSIZE][COLSIZE] с добавлением пользователя #define RAM_UINT8 __attribute__ ((section ("EXTRAM"))) uint8_t Недостатком этого является то, что требуется, чтобы все данные находились в одном непрерывном разделе ОЗУ, что не всегда возможно для встроенной цели.

Интересно, есть ли еще варианты? Прямо сейчас я склоняюсь к варианту 4, так как он достаточно опрятен, но не влияет на реальную производительность во время выполнения.

Подводя итог: Каков наилучший способ реализации уровня абстракции хранилища с низким / нулевым объемом памяти на Cortex-M4?

2 ответа

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

Учитывая, что вы уже делаете

row*column + offset + size*parameter 

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

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

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

Так что лично я бы склонялся к решениям, аналогичным тем, которые перечислены выше как 3/4/5. Я бы не стал вдаваться в чрезмерно сложные шаблоны и шаблоны ООП (сначала), а вместо этого попытался бы найти фактическое узкое место "табличного модуля", подобного этому, путем проведения тестов и измерения его реальной производительности. И получите больше контроля над фактической разметкой памяти и операциями доступа. И постарайся сделать это простым.:)

Не уверен, решит ли это вашу проблему, но вот некоторые общие мысли на эту тему:

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

  • Фиксированные размеры с степенью двойки: чтобы ускорить процесс, можно использовать записи таблицы размером 2^n, что, вероятно, приведет к более быстрому доступу благодаря использованию операций сдвига битов /-измерения вместо умножения /etc (строка и запись размер числа записей / байтов в степени двух, например размер записи таблицы 256 байтов с 64 x 32-битными элементами). Предполагая, что ваше приложение позволяет это, вы можете округлить размер записей таблицы до следующей степени двойки и оставить неиспользованными некоторые байты - скорость против размера.

При использовании таблицы с фиксированным размером два, доступ к массиву может быть записан явно как добавление указателей, так что код больше напоминает то, что на самом деле должен делать процессор. Стоит учитывать только критически важные для производительности части (это скорее дело вкуса - компилятор, вероятно, будет делать то же самое при использовании нотации массива):

   //return table[row][column].parameters[parameterID];

   //const entry *e = table + column * table_width + row;
   //return entry->parameterID;

   //#define COL(col) ((col) * ROW_SIZE)
   //#define ROW(row) ((row) * ENTRY_SIZE)
   //#define PARAM(param) ((param) * PARAM_SIZE)
   #define COL(col) ((col) << SHIFT_COL_SIZE)
   #define ROW(row) ((row) << SHIFT_ROW_SIZE)
   //#define PARAM(param) ((param) << SHIFT_PARAM_SIZE) // (PARAM_SIZE == 4)?

   param *p = table + COL(column) + ROW(row) + parameterID; //PARAM(parameterID);
   // Do something with p? Return p instead of *p?
   return *p;

Это работает только тогда, когда размеры таблицы известны во время компиляции, поэтому вам, вероятно, понадобится более динамичное решение и пересчитать приращения / число битовых сдвигов при изменении размера таблицы. Может быть, запись таблицы и размер параметра могут быть фиксированными, так что во время компиляции не нужно будет знать только размеры / сдвиги строк / столбцов?

  • inlineИспользование функций может помочь уменьшить накладные расходы при вызове функции.

  • Пакетная обработка: Выполнение нескольких обращений в последовательности, вероятно, более эффективно, чем доступ к отдельным записям. Вы можете использовать указатель арифметики, чтобы сделать это.

  • Выравнивание памяти: выровняйте все записи по 4-байтовым словам и сделайте записи не менее 4-х байтов. Насколько я знаю, это помогает STM32 с доступом к памяти.

  • DMA: использование памяти в памяти DMA может сильно помочь со скоростью.

  • Периферийное устройство FMC STM32F4x: Если вы используете внешнюю SDRAM, все может быть изменено с использованием различных временных параметров (FMC). В функциях HAL_SDRAM_*(), предоставляемых ST, могут быть полезные фрагменты кода.

  • Кеш: Поскольку Cortex-M4 не имеет кеша данных / инструкций (AFAIK), весь магический кеш вуду можно безопасно игнорировать.:)

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

Другие вопросы по тегам