嵌入式目标C++:低开销存储后端

C++ on embedded targets: Low overhead storage backend

本文关键字:开销 存储 后端 目标 C++ 嵌入式      更新时间:2023-10-16

我正在为ARM Cortex-M4处理器编写可重用的C++模块。该模块使用大量存储空间来完成其任务,并且时间紧迫

为了允许我的模块的用户自定义其行为,我使用不同的后端类来允许不同的低级任务实现。其中一个后端是存储后端,旨在将实际数据存储在不同类型的易失性/非易失性RAM中。它主要由执行速度非常快的set/get函数组成,并且它们将被非常频繁地调用。它们大多采用以下形式:

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

底层表和数组的大小和数据类型取决于用户定义的功能,因此我无法使用存储后端进行 aviod。一个主要问题是需要将实际数据放入RAM地址空间的某个部分(例如,用于使用外部RAM),并且我不想将我的模块限制为特定的存储选项。

现在我想知道选择哪种设计模式来将存储方面与我的主模块分开。

  1. 具有虚函数的类将是一个简单而强大的选择。但是,我担心在时间紧迫的环境中经常调用虚拟 set/get 函数的成本。特别是对于存储后端,这可能是一个严重的问题。
  2. 为模块主类提供其不同后端的模板参数(甚至可能使用 CRTP 模式?这将避免虚拟函数,甚至允许内联存储后端的设置/获取函数。但是,它需要整个主类在头文件中实现,这不是特别整洁......
  3. 使用简单的 C 样式函数来形成存储后端。
  4. 对简单的 set/get 函数使用宏(编译后,这应该与选项 2 大致相同,所有 set/get 函数都内联。
  5. 自己定义存储数据结构,并允许使用宏作为数据类型进行自定义。 例如 RAM_UINT8 table[ROWSIZE][COLSIZE]用户添加#define RAM_UINT8 __attribute__ ((section ("EXTRAM"))) uint8_t 这样做的缺点是,它要求所有数据都位于RAM的同一连续部分中 - 这在嵌入式目标上并不总是可行的。

我想知道是否还有更多选择?现在,我倾向于选项 4,因为它足够整洁,但它对实际运行时性能的影响为零。

总结一下:在 Cortex-M4 上实现低/零开销存储抽象层的最佳方法是什么?

虚拟成员通常归结为一个额外的查找(如果那样的话)。 虚拟函数的 vtable(一种常见的实现方法)通常可以从"this"指针轻松访问,使用的指令不大于将已知固定地址加载到静态链接函数的通常指令。

鉴于你已经在做

row*column + offset + size*parameter 

假设您没有重载任何运算符)并且您正在调用一个传递 3 个参数(都需要加载)的函数,如果有的话,这是一个相当小的开销。

但是,这并不是

说,如果您进行大量访问,调用函数的开销不会烧毁您。 但是,答案是允许您一次检索多个值。

根据我的经验,语言特征很少有助于解决具体问题。它们可以提高代码的可维护性、可读性和模块化。让它更优雅、更漂亮,有时更有效率,但就我个人而言,我不会过分依赖语言功能和编译器,尤其是微控制器。

因此,就个人而言,我倾向于类似于上面列出的 3/4/5 的解决方案。我会避免进入过于复杂的模板和 OOP 模式(起初),而是尝试通过进行测试和测量其实际性能来找到像这样的"表模块"的实际瓶颈。并更好地控制实际内存布局和访问操作。并尽量保持简单。:)

不确定,这是否解决了您的问题,但这里有一些关于这个主题的一般想法:

    扁平结构
  • :您可以使用扁平内存结构,而不是使用多维数组。这样,可以更轻松地优化对单个条目的访问以提高速度,并且您可以完全控制数据布局。更重要的是,如果所有数据元素都具有固定的、相等的大小。

  • 固定的,两个幂的大小:为了加快速度,您可以使用 2^n 大小的表条目,这可能会通过使用位移/-wise 运算而不是乘法/等(行和条目大小为两个次幂的条目/字节,例如表条目大小为 256 字节,具有 64 x 32 位元素)来加快访问速度。假设您的应用程序允许这样做,您可以将表条目的大小舍入到下一个 2 次方,并保留一些未使用的字节 - 速度与大小。

对于固定的 2 次幂大小表,数组访问可以显式编写为指针的添加,以便代码更接近处理器实际应该执行的操作。仅在性能关键部分值得考虑(更多的是品味问题 - 当使用数组表示法时,编译器可能会做同样的事情):

   //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可能会对速度有很大帮助。

  • STM32F4x的FMC外设:如果您使用外部SDRAM,则可以通过使用不同的时序参数(FMC)进行调整。ST 提供的 HAL_SDRAM_*() 函数中可能有有用的代码。

  • 缓存
  • :由于Cortex-M4没有数据/指令缓存(AFAIK),因此可以安全地忽略所有魔法缓存巫毒教。 :)

  • (数据结构
  • :根据数据的性质和访问方法,不同的数据结构可能很有用。如果表可能在运行时调整大小,并且随机访问不是那么重要,则链表可能会很有趣。或者哈希表可能值得一看。