我正在为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),并且我不想将我的模块限制为特定的存储选项。
现在我想知道选择哪种设计模式来将存储方面与我的主模块分开。
- 具有虚函数的类将是一个简单而强大的选择。但是,我担心在时间紧迫的环境中经常调用虚拟 set/get 函数的成本。特别是对于存储后端,这可能是一个严重的问题。
- 为模块主类提供其不同后端的模板参数(甚至可能使用 CRTP 模式?这将避免虚拟函数,甚至允许内联存储后端的设置/获取函数。但是,它需要整个主类在头文件中实现,这不是特别整洁......
- 使用简单的 C 样式函数来形成存储后端。
- 对简单的 set/get 函数使用宏(编译后,这应该与选项 2 大致相同,所有 set/get 函数都内联。
- 自己定义存储数据结构,并允许使用宏作为数据类型进行自定义。 例如
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),因此可以安全地忽略所有魔法缓存巫毒教。 :)
(数据结构:根据数据的性质和访问方法,不同的数据结构可能很有用。如果表可能在运行时调整大小,并且随机访问不是那么重要,则链表可能会很有趣。或者哈希表可能值得一看。