以最小的重复和代码大小静态分配派生类的实例数组和基类的指针数组

问题描述 投票:0回答:1

首先,我在嵌入式环境中工作(针对 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

c++ arduino embedded teensy
1个回答
0
投票

因此,我已经确定了两个问题的答案并解决了基本问题,但对更深入了解为什么的其他答案感兴趣:

1.为什么我无法在 godbolt 中重现该问题?

因为我的基类中没有任何虚拟方法(就像我在实际项目中所做的那样)。添加虚拟方法会导致二进制大小如实际项目中所观察到的那样膨胀。请参阅 https://godbolt.org/z/5qncncffE 了解该问题的重现。

2.如何修改基于模板的代码以防止二进制大小/内存使用增加?

只需将

pool
字段标记为
static inline
(这对我来说没问题;无论如何它是一个单例)就可以消除这个问题。请参阅https://godbolt.org/z/W83f9aqPP

© www.soinside.com 2019 - 2024. All rights reserved.