Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Statically allocate arrays of instances of derived class and arrays of pointers of base class with minimal repetition and code size

First, I'm working in an embedded environment (targeting Teensy 3.2 and 4.1); thus keeping memory usage and compiled code size at a minimum is critical. This question centers around refactoring some code without incurring a penalty in compiled code size and memory.

In response to votes to close: this code is not working as expected; it does not have the expected memory usage or compilation size characteristics. It is also example code and thus not suitable for code review. Finally, while I give a lot of context for my situation, this is a generalized question about why different ways of statically allocating resources increase compilation size and memory usage.

I have ~70 derived classes from a single base class. For example:

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];
};

Each derived class has 4 instances. An array of structs containing metadata and Base class pointers points to these instances, allowing the rest of the code base to interact with them polymorphically. The code was originally implemented as follows (with the help of macros):


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] }},
};

While macros can make this somewhat less tedious, they have been fragile, and require listing out the derived class names twice. In an effort to clean up this code, make it easier to maintain, and allow for easier future improvements, I'm trying to replace this with template-based definitions in which the derived class names need only be listed once with their accompanying metatada. My strategy has been to create the pool of instances inside tuples. Defining the tuples and initializing the array of wrappers is made pretty straightforward via template parameter unpacking:

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},
};

Testing this out with godbolt, ~it seems to work great~ (see next paragraph): https://godbolt.org/z/8fhhTjEM1. With the constructor declared constexpr, all template specializations and such are discarded during compilation, leaving pretty much just the requisite memory allocation of the instances, pointers, and metadata, almost exactly the same as the explicit code above. Note that minor modifications disrupt this. For example, switching from an array of tuples to a tuple of arrays causes the compiled size to balloon with template specializations (see commented out code in the link).

Update: I figured out that the reason it looked okay on godbolt and not in project was because my base class didn't have any virtual methods in godbolt. Simply adding a virtual method (without even overriding it) causes binary size to balloon again: https://godbolt.org/z/5qncncffE

When I use this technique in the real project, memory and code size grow significantly. If I move the tuples outside of the class like so:

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]),
    }}...} {}
};

Then memory and code size remain at about the same level as with the original code. The problem is that this requires listing out the derived classes two times (again, there's about 70 of them).

Why might having the tuple pools inside the class cause an increase in compiled code and memory size? The change in size is similar to removing the constexpr decorator on the constructor.

Memory/code size with constexpr and pool outside of class:

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

Memory/code size with constexpr and pool inside of class:

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

Memory/code size without constexpr and pool outside of class:

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

Memory/code size of original

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

Compiling with arm-none-eabi-g++ 11.3.1, using the following relevant build flags:

-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 

This godbolt should offer a pretty accurate reproduction and includes a virtual method in the base class which causes the issue to reproduce: https://godbolt.org/z/5qncncffE

like image 873
Bryan Head Avatar asked Nov 18 '25 17:11

Bryan Head


1 Answers

So I've determined the answer to two of my questions and solved the basic problem, but would be interested in other answers that go deeper into the why:

1. Why couldn't I reproduce the issue in godbolt?

Because I didn't have any virtual methods in my base class (as I do in the real project). Adding a virtual method causes binary size to balloon as observed in the real project. See https://godbolt.org/z/5qncncffE for a reproduction of the issue.

2. How can I modify the template-based code to prevent the increase in binary size/memory use?

Simply marking the pool field as static inline (which is fine in my case; its a singleton anyway) eliminates the issue. See https://godbolt.org/z/W83f9aqPP

This results in the following in the real project:

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

which is identical to moving the pool outside the class. This makes sense, since that's basically what marking it static is doing.

like image 196
Bryan Head Avatar answered Nov 20 '25 06:11

Bryan Head



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!