Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid C++ code bloat issued by template instantiation and symbol table?

I'd started a bare-metal (Cortex-M) project some years ago. At project setup we decided to use gcc toolchain with C++11 / C++14 etc. enabled and even for using C++ exceptions and rtti.

We are currently using gcc 4.9 from launchpad.net/gcc-arm-embedded (having some issue which prevent us currently to update to a more recent gcc version).

For example, I'd wrote a base class and a derived class like this (see also running example here):

class OutStream {
public:
    explicit OutStream() {}
    virtual ~OutStream() {}
    OutStream& operator << (const char* s) {
        write(s, strlen(s));
        return *this;
    }
    virtual void write(const void* buffer, size_t size) = 0;    
};

class FixedMemoryStream: public OutStream {
public:
    explicit FixedMemoryStream(void* memBuffer, size_t memBufferSize): memBuffer(memBuffer), memBufferSize(memBufferSize) {}
    virtual ~FixedMemoryStream()       {}
    const void*  getBuffer() const     { return memBuffer; }
    size_t       getBufferSize() const { return memBufferSize; }
    const char*  getText() const       { return reinterpret_cast<const char*>(memBuffer); }  ///< returns content as zero terminated C-string    
    size_t       getSize() const       { return index; }                                     ///< number of bytes really written to the buffer (max = buffersize-1)
    bool         isOverflow() const    { return overflow; }
    virtual void write(const void* buffer, size_t size) override { /* ... */ }
private:
    void*  memBuffer = nullptr;   ///< buffer
    size_t memBufferSize = 0;     ///< buffer size
    size_t index = 0;             ///< current write index
    bool   overflow = false;      ///< flag if we are overflown
};

So that the customers of my class are now able to use e.g.:

char buffer[10];
FixedMemoryStream ms1(buffer, sizeof(buffer));
ms1 << "Hello World";

Now I'd want to make the usage of the class a bit more comfortable and introduced the following template:

template<size_t bufferSize> class FixedMemoryStreamWithBuffer: public FixedMemoryStream {
public:
    explicit FixedMemoryStreamWithBuffer(): FixedMemoryStream(buffer, bufferSize) {}
private:
    uint8_t buffer[bufferSize];
};

And from now, my customers can write:

FixedMemoryStreamWithBuffer<10> ms2;
ms2 << "Hello World";

But from now, I'd observed increasing size of my executable binary. It seems that gcc added symbol information for each different template instantiation of FixedMemoryStreamWithBuffer (because we are using rtti for some reason).

Might there be a way to get rid of symbol information only for some specific classes / templates / template instantiations?

It's ok to get a non portable gcc only solution for this.

For some reason we decided to prefer templates instead of preprocessor macros, I want to avoid a preprocessor solution.

like image 825
Joe Avatar asked Dec 19 '17 19:12

Joe


2 Answers

First of all, keep in mind that compiler also generates separate v-table (as well as RTTI information) for every FixedMemoryStreamWithBuffer<> type instance, as well as every class in the inheritance chain.

In order to resolve the problem I'd recommend using containment instead of inheritance with some conversion function and/or operator inside:

    template<size_t bufferSize> 
    class FixedMemoryStreamWithBuffer
    {
         uint8_t buffer[bufferSize];
         FixedMemoryStream m_stream;
    public:
        explicit FixedMemoryStreamWithBuffer() : m_stream(m_buffer, bufferSize) {}
        operator FixedMemoryStream&() { return m_stream; }
        FixedMemoryStream& toStream() { return m_stream; }
   };
like image 105
Alex Robenko Avatar answered Oct 15 '22 13:10

Alex Robenko


Yes, there's a way to bring the necessary symbols almost down to 0: using the standard library. Your OutStream class is a simplified version of std::basic_ostream. Your OutStream::write is really just std::basic_ostream::write and so on. Take a look at it here. Overflow is handled really closely, though, for completeness' sake, it also deals with underflow i.e. the need for data retrieval; you may leave it as undefined (it's virtual too).

Similarly, your FixedMemoryStream is std::basic_streambuf<T> with a fixed-size (a std::array<T>) get/put area.

So, just make your classes inherit from the standard ones and you'll cut off on binary size since you're reusing already declared symbols.


Now, regarding template<size_t bufferSize> class FixedMemoryStreamWithBuffer. This class is very similar to std::array<std::uint8_t, bufferSize> as for the way memory is specified and acquired. You can't optimize much about that: each instantiation is a different type with all what that implies. The compiler cannot "merge" or do anything magic about them: each instantiation must have its own type. So either fall back on std::vector or have some fixed-size specialized chunks, like 32, 128 etc. and for any values in between would choose the right one; this can be achieved entirely at compile-time, so no runtime cost.

like image 1
edmz Avatar answered Oct 15 '22 11:10

edmz