【问题标题】:Make C++ class partially constexpr and save RAM使 C++ 类部分 constexpr 并节省 RAM
【发布时间】:2020-12-05 10:04:56
【问题描述】:

我为一个有按钮和灯的控制器编写了代码。它基于 Arduino/ATmega2560。 RAM非常有限。无法更改为其他设备。

每个按钮和灯都被建模为一个 C++ 类实例。它们派生自抽象类Thing,其中包含一个指针mpTarget,指示Things 之间的连接。目标最初是在运行时分配的,在运行时不会更改。

我需要节省内存。因为我有大约 600 个 Things,如果所有这些 mpTargets 都存储在 ROM(而不是 RAM)中,这将对我有很大帮助,因为目标在编译时是已知的。

所以我玩了constexpr。但是,似乎不可能只部分地创建一个类constexpr。我无法创建整个类constexpr,因为还有一些成员变量在运行时会发生变化(下例中的mState)。

什么是实现这种行为的好方法?常量表达式?模板?有什么事吗?

澄清

  1. 我知道mpTarget 当前是在运行时初始化的。但是每个目标在编译时都是已知的,这就是为什么我想找到一种保存这些指针的 RAM 字节的好方法。

  2. 我真的不想离开面向对象的设计。我之前使用了一种简单的“普通旧”数据类型的方法(实际上消耗的 RAM 更少)。但由于此代码的开发仍在进行中(添加/删除/修改类和实例),很有可能会错过必要的修改,从而引入难以发现的错误。

  3. 对于这个项目,只为mpTarget 保存 RAM 存储就绰绰有余了。保持 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);
  }
}

【问题讨论】:

  • 变量是存储在 RAM 还是 ROM 中,不能完全由 constexpr 说明符控制。因此,它会使前提变弱,否则是一个很好的问题。
  • 这能回答你的问题吗? g++ compiler flag to minimize binary size EDIT 我现在意识到你想要做的是不同的。无论如何,我在那里链接的问题的上下文可能对您来说很有趣,所以我将其留在这里。
  • 您如何创建/初始化您的 600 Things?
  • @Mat - 我像上面的例子一样初始化所有东西。全局范围内的 600 个实例,无参数。然后,在运行时开始时,为每个实例调用 setTarget()。
  • @Mat - 你是对的 - 有很多闪存,我只使用了 20% 的闪存,但使用了 ATmega 的 8k RAM 的 80%。

标签: c++ c++11


【解决方案1】:

从头开始重新设计

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 bytes SRAM 的设备来说至关重要。

【讨论】:

  • 这个“面向属性的设计”是基于一个 cppcon 谈话。不幸的是,我不记得它是怎么称呼的,也找不到谈话内容。
  • ..不知道,对我来说它看起来基于“编程艺术”。 WH 早于大多数现代语言。
  • 感谢您的回答。实际上,这样的方法与我之前使用的方法相似。但是因为最终在编辑代码时有太多“摆弄”,这就是我从 OOP 方法开始的原因。请注意,以上只是一个最低限度的示例;实际上有大约 20 个类,也有不同的关系。
【解决方案2】:
  1. 使用小状态
  2. 消除所有虚拟方法(可能还有所有基类)以减少类状态。
  3. 使用非类型模板参数将引用从数据 (RAM) 移动到代码 (ROM)

尽可能使用基于字符的enum 表示状态:

enum binary_enable_state : char {
    BINARY_ENABLE_ON,
    BINARY_ENABLE_OFF
};

事件侦听器应该只是侦听,没有指针、引用或任何真实状态。

class Lamp {
public:
  void doSomething(binary_enable_state value)
  {
    std::cout << value << std::endl;
  };
};

事件源应该对监听器使用非类型模板参数引用,从而避免指针和状态。

template<class Listener, Listener& mpTarget>
class Button {

  public:
  void setState(binary_enable_state newState)
  {
    mState = newState;
    mpTarget.doSomething(mState);
  }
  
  private:
  // changes during runtime
  binary_enable_state mState; //Do you actually need to store this?
};

这基本上将图形放在代码本身 (ROM) 中,而不是作为 RAM 中的数据。

Lamp myLamp;
Button<decltype(myLamp), myLamp> myButton;

int main()
{
    myButton.setState(BINARY_ENABLE_ON);
}

http://coliru.stacked-crooked.com/a/7171e4b9547ccbe1

【讨论】:

  • 谢谢,我接受了这个答案,因为这解决了我的主要问题(如何将图形放入 ROM 而不是 RAM)。该解决方案效果很好,为每个指针节省了 4 个 RAM 字节,这对于我的应用程序来说绰绰有余。
  • 还有一件事。假设我的图表有点复杂,例如:Lamp myLamp; Relay&lt;Lamp, myLamp&gt; myRelay; Button&lt;Relay&lt;Lamp, myLamp&gt;, myRelay&gt; myButton; - 我必须编写嵌套模板参数列表。如何避免?
  • using MyLampRelay = Relay&lt;Lamp, myLamp&gt;; ?
  • 更好:Button&lt;decltype(myRelay), myRelay&gt; myButton;
  • 太棒了!非常感谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-01-01
  • 2018-02-18
  • 2019-09-08
  • 2022-01-11
  • 2021-03-08
  • 2013-05-12
  • 1970-01-01
相关资源
最近更新 更多