使C++类部分constexpr并节省RAM
我为具有按钮和灯的控制器编写了代码。它基于 Arduino/ATmega2560。RAM非常有限。无法更改为其他设备。
每个按钮和灯都被建模为一个 C++ 类实例。它们派生自一个抽象类Thing,其中包含一个指示smpTarget之间连接的指针Thing。目标最初是在运行时分配的,在运行时不会更改。
我需要节省内存。由于我有大约 600Thing秒,如果所有这些mpTargets 都存储在 ROM(而不是 RAM)中,这将对我有很大帮助,因为目标在编译时是已知的。
所以我玩了constexpr. 但是,似乎不可能仅部分地创建一个类constexpr。我无法创建整个类constexpr,因为还有一些成员变量会在运行时发生变化(mState在下面的示例中)。
实现这种行为的好方法是什么?常量表达式?模板?任何事物?
澄清
-
我知道
mpTarget当前是在运行时初始化的。但是每个目标在编译时都是已知的,这就是为什么我想找到一种保存这些指针的 RAM 字节的好方法。 -
我真的不想摆脱面向对象的设计。我之前使用了一种简单的“普通”数据类型的方法(实际上消耗的 RAM 更少)。但由于此代码的开发仍在进行中(添加/删除/修改类和实例),很可能会错过必要的修改,从而引入难以发现的错误。
-
对于这个项目,只为
mpTarget. 保持 vtable 指针的开销很好。我实际上正在寻找的是 - 正如标题所暗示的 - 我们如何实现一个成员部分为constexpr. 我也考虑过使用模板,但没有成功。
示例代码
#include <iostream>
class Thing
{
public:
// only called in setup()
void setTarget(Thing & pTarget)
{
mpTarget = & pTarget;
}
virtual void doSomething(int value) {};
protected:
// known at compile time - can we make this constexpr?
Thing * mpTarget;
};
class Button : public Thing
{
public:
void setState(int newState)
{
mState = mState + newState;
mpTarget->doSomething(mState);
}
private:
// changes during runtime
int mState;
};
class Lamp: public Thing
{
public:
virtual void doSomething(int value)
{
std::cout << value << std::endl;
};
};
Button myButton;
Lamp myLamp;
int main()
{
myButton.setTarget(myLamp);
while (1)
{
myButton.setState(123);
}
}
回答
从头开始重新设计
ATmega2560 只有 8KB SRAM。与正常的桌面标准相比,这是极低的。600 个具有 3 个 2 字节属性的对象,每个对象几乎可以填充一半的可用内存。
在这种限制性环境中编程迫使您从一开始就围绕硬件限制调整整个设计。编写普通代码,然后尝试将其安装到您的硬件中,但事实并非如此。这是“首先编写可读代码,然后尝试对其进行优化”的一个很好的例外。
一个想法:按属性分组,而不是按对象
首先你需要放弃虚方法。这将至少为每个实例添加一个 vtable 指针。在 600 个实例时,这是一个非常沉重的成本。
我在这里设计的一个想法是完全放弃 OOP。或者至少部分。不是按实例分组属性,而是将所有属性组合在向量中。
这有几个很大的优点:
- 它通过放弃 vtable 来节省空间
- 因为属性是如何分组的,所以可以将编译时已知的属性存储在 ROM 中
- 它通过使用每个属性所需的最少位数来节省空间
例子
为了举例,让我们考虑这种情况:
- 我们有 100 个灯、200 个按钮和 300 个 LED
- 按钮和 LED 有一个
target属性。只有灯可以成为目标 - 按钮有
state属性。有 2 种可能的状态(开/关) - LED 具有
color属性。有 16 种可能的预定义颜色
下面的例子使用文字是显式的,但在代码中你应该使用常量(例如NUM_BUTTONS等)
target 财产
我们有 500 个(200 个按钮 + 300 个 LED)具有target属性的对象。所以我们需要一个大小为 的向量500。有 100 个目标。所以数据类型适合int8_t. 所以目标看起来像这样:
constexpr int8t_t targets[500] = ...
在这个向量中,前 200 个元素代表按钮的目标,接下来的 300 个元素代表 LED 的目标。该向量将存储在 ROM 中。
要获得事物的目标,我们有:
int8_t button_target(int button) { return targets[button]; }
int8_t led_target(int led) { return targets[200 + led]; }
或者使用两个向量:
constexpr int8t_t button_targets[200] = ...
constexpr int8t_t led_targets[300] = ...
在编译时填充此向量是您需要解决的问题。我不知道您现在是如何创建对象的。您可以对代码中的值进行硬编码,也可以生成代码。
state 财产
我们有 200 个带状态的元素。由于有 2 种可能的状态,我们只需要每个状态 1 位。所以我们只需要200 / 8 = 25字节:
int8_t state[25] = {};
获取和设置按钮的状态比较复杂,它需要位掩码操作,但我们在这里节省了 175 个字节(87.5% 的数据节省了空间)并且每个字节都很重要。
bool button_get_state(int button)
{
int8_t byte = state[button / 8];
return byte & (1 << (button % 8));
}
color 财产
我们有 300 个带颜色的元素。由于有 16 种颜色,我们需要每种颜色 4 位,因此我们可以对每个字节编码 2 种颜色:
int8_t color[150] = {};
再次获取和设置颜色需要一些摆弄。
结论
正如您所看到的,这种设计远没有 OOP 漂亮。并且它是否需要更复杂的代码。但它有一个很大的优势,那就是为你节省了大量的空间,这在只有8,192 bytesSRAM的设备上是最重要的。