首先,我在嵌入式环境中工作(针对 Teensy 3.2 和 4.1);因此,将内存使用量和编译代码大小保持在最低限度至关重要。这个问题的中心是重构一些代码,而不会导致编译代码大小和内存的损失。
回应投票结束:此代码未按预期工作;它不具有预期的内存使用或编译大小特征。它也是示例代码,因此不适合代码审查。最后,虽然我针对我的情况提供了很多背景信息,但这是一个普遍的问题,即为什么静态分配资源的不同方式会增加编译大小和内存使用量。
我有大约 70 个来自单个基类的派生类。例如:
class Base {
public:
int x;
};
class Derived1 : public Base {
public:
int arr[1];
};
class Derived2 : public Base {
public:
int arr[2];
};
class Derived3 : public Base {
public:
int arr[3];
};
每个派生类有 4 个实例。包含元数据和基类指针的结构数组指向这些实例,允许代码库的其余部分与它们进行多态交互。代码最初实现如下(借助宏):
Derived1 Derived1_instances[4];
Derived2 Derived2_instances[4];
Derived3 Derived3_instances[4];
struct Wrapper {
int id;
unsigned char category;
std::array<Base*, 4> instances;
};
Wrapper wrappers[] = {
{ 0, 0b001, {&Derived1_instances[0], &Derived1_instances[1], &Derived1_instances[2], &Derived1_instances[3] }},
{ 0, 0b010, {&Derived2_instances[0], &Derived2_instances[1], &Derived2_instances[2], &Derived2_instances[3] }},
{ 0, 0b100, {&Derived3_instances[0], &Derived3_instances[1], &Derived3_instances[2], &Derived3_instances[3] }},
};
虽然宏可以让这件事变得不那么乏味,但它们很脆弱,并且需要列出派生类名称两次。为了清理此代码,使其更易于维护,并允许将来更轻松地进行改进,我尝试将其替换为基于模板的定义,其中派生类名称及其随附的元数据只需列出一次。我的策略是在元组内创建实例池。通过模板参数解包,定义元组和初始化包装数组变得非常简单:
template <class D>
struct Declaration {
int id;
unsigned char category;
};
template <class... Classes>
struct Registry {
std::tuple<Classes...> pool[4];
std::array<Wrapper, sizeof...(Classes)> Wrappers;
constexpr Registry(Declaration<Classes>... decls) :
Wrappers{Wrapper{decls.id, decls.category, {
&std::get<Classes>(pool[0]),
&std::get<Classes>(pool[1]),
&std::get<Classes>(pool[2]),
&std::get<Classes>(pool[3]),
}}...} {}
};
Registry reg{
Declaration<Derived1>{0, 0b001},
Declaration<Derived2>{1, 0b010},
Declaration<Derived3>{2, 0b100},
};
用 godbolt 对此进行测试,〜似乎效果很好〜(请参阅下一段):https://godbolt.org/z/8fhhTjEM1。通过声明
constexpr
的构造函数,所有模板特化等都会在编译期间被丢弃,几乎只留下实例、指针和元数据所需的内存分配,几乎与上面的显式代码完全相同。请注意,微小的修改会破坏这一点。例如,从元组数组切换到数组元组会导致编译后的大小随着模板专业化而增大(请参阅链接中注释掉的代码)。
更新:我发现它在 godbolt 上看起来不错而不是在项目中的原因是因为我的基类在 godbolt 中没有任何虚拟方法。只需添加一个虚拟方法(甚至不覆盖它)就会导致二进制大小再次膨胀:https://godbolt.org/z/5qncncffE
当我在实际项目中使用这种技术时,内存和代码大小显着增加。如果我像这样将元组移到类之外:
std::tuple<Derived1, Derived2, Derived3> pool[4];
template <class... Classes>
struct Registry {
std::array<Wrapper, sizeof...(Classes)> Wrappers;
constexpr Registry(Declaration<Classes>... decls) :
Wrappers{Wrapper{decls.id, decls.category, {
&std::get<Classes>(pool[0]),
&std::get<Classes>(pool[1]),
&std::get<Classes>(pool[2]),
&std::get<Classes>(pool[3]),
}}...} {}
};
然后内存和代码大小保持在与原始代码大致相同的水平。问题是,这需要列出派生类两次(同样,大约有 70 个)。
为什么在类中使用元组池可能会导致编译代码和内存大小增加?大小的变化类似于删除构造函数上的
constexpr
装饰器。
内存/代码大小与
constexpr
和课外池:
teensy_size: Memory Usage on Teensy 4.1:
teensy_size: FLASH: code:300156, data:105996, headers:8564 free for files:7711748
teensy_size: RAM1: variables:188608, code:291744, padding:3168 free for local variables:40768
teensy_size: RAM2: variables:52320 free for malloc/new:471968
内存/代码大小与
constexpr
和类内的池:
teensy_size: Memory Usage on Teensy 4.1:
teensy_size: FLASH: code:303820, data:103948, headers:8996 free for files:7709700
teensy_size: RAM1: variables:190368, code:295408, padding:32272 free for local variables:6240
teensy_size: RAM2: variables:52320 free for malloc/new:471968
没有
constexpr
和课外池的内存/代码大小:
teensy_size: Memory Usage on Teensy 4.1:
teensy_size: FLASH: code:303708, data:103948, headers:9108 free for files:7709700
teensy_size: RAM1: variables:190368, code:295296, padding:32384 free for local variables:6240
teensy_size: RAM2: variables:52320 free for malloc/new:471968
原件内存/代码大小
teensy_size: Memory Usage on Teensy 4.1:
teensy_size: FLASH: code:300732, data:105996, headers:9012 free for files:7710724
teensy_size: RAM1: variables:188576, code:292320, padding:2592 free for local variables:40800
teensy_size: RAM2: variables:52320 free for malloc/new:471968
使用arm-none-eabi-g++ 11.3.1进行编译,使用以下相关构建标志:
-fno-exceptions -felide-constructors -fno-rtti -std=gnu++17 -Wno-error=narrowing -fpermissive -fno-threadsafe-statics -Wall -Wfatal-errors -ffunction-sections -fdata-sections -mthumb -mcpu=cortex-m7 -nostdlib -mfloat-abi=hard -mfpu=fpv5-d16 -Os --specs=nano.specs
这个 godbolt 应该提供相当准确的再现,并在基类中包含一个虚拟方法,这会导致问题再现:https://godbolt.org/z/5qncncffE
因此,我已经确定了两个问题的答案并解决了基本问题,但对更深入了解为什么的其他答案感兴趣:
因为我的基类中没有任何虚拟方法(就像我在实际项目中所做的那样)。添加虚拟方法会导致二进制大小如实际项目中所观察到的那样膨胀。请参阅 https://godbolt.org/z/5qncncffE 了解该问题的重现。
只需将
pool
字段标记为 static inline
(这对我来说没问题;无论如何它是一个单例)就可以消除这个问题。请参阅https://godbolt.org/z/W83f9aqPP