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?

3

Решение

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

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

row*column + offset + size*parameter

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

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

0

Другие решения

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

Так что лично я бы склонялся к решениям, аналогичным тем, которые перечислены выше как 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), весь магический кеш-вуду можно безопасно игнорировать. 🙂

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

0

По вопросам рекламы [email protected]