【问题标题】:How to avoid C++ code bloat issued by template instantiation and symbol table?如何避免模板实例化和符号表导致的 C++ 代码膨胀?
【发布时间】:2018-06-02 06:44:04
【问题描述】:

几年前我开始了一个裸机 (Cortex-M) 项目。在项目设置中,我们决定使用启用 C++11 / C++14 等的 gcc 工具链,甚至使用 C++ 异常和 rtti。

我们目前正在使用gcc 4.9 from launchpad.net/gcc-arm-embedded(存在一些问题,导致我们目前无法更新到更新的 gcc 版本)。

例如,我写了一个这样的基类和一个派生类(另见运行示例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
};

以便我班的客户现在可以使用例如:

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

现在我想让这个类的使用更舒适一些,并引入了以下模板:

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

从现在开始,我的客户可以写:

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

但是从现在开始,我发现我的可执行二进制文件的大小越来越大。似乎 gcc 为 FixedMemoryStreamWithBuffer 的每个不同模板实例添加了符号信息(因为我们出于某种原因使用 rtti)。

有没有办法只为某些特定的类/模板/模板实例化去除符号信息?

可以为此获得一个非便携式 gcc 唯一的解决方案。

出于某种原因,我们决定更喜欢模板而不是预处理器宏,我想避免使用预处理器解决方案。

【问题讨论】:

  • 如果去掉虚函数怎么办?只有多态对象才能使用 RTTI。
  • 虚函数是绝对需要的,因为我还有一些其他类派生自OutStream(例如 UartStream、TcpStream 等)。
  • 只有在多态地使用这些派生类时才需要(即通过基类类型的表达式)。
  • operator &lt;&lt; 在嵌入式系统中是一个糟糕的接口。它需要每种类型的实例来“流式传输”,因此您最终会在二进制文件中得到许多类似的序言/尾声。您应该尝试改用类似 printf 的解决方案(因为 va_list,尽管它们看起来很糟糕,但在二进制大小上却是相当理想的)。

标签: c++11 templates gcc bare-metal


【解决方案1】:

首先,请记住,编译器还会为每个 FixedMemoryStreamWithBuffer 类型实例以及继承链中的每个类生成单独的 v-table(以及 RTTI 信息)。

为了解决这个问题,我建议使用包含而不是继承,其中包含一些转换函数和/或运算符:

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

【讨论】:

  • 有趣的方法。由于FixedMemoryStreamWithBuffer 不再是多态的,编译器有真正的机会摆脱符号信息。但不幸的是,我必须从现在开始像ms.toStream() &lt;&lt; "Hallo World"; 一样调用toStream() 方法,因为在这种情况下强制转换运算符不会隐式工作。
  • @Joe,但是是什么阻止您定义自己的“操作员
  • 是的,我必须为我的OutStream 现在和将来支持的所有数据类型复制operator&lt;&lt;。这绝对违反了Dry
  • @Joe 我不同意。你忘记了模板。单个模板运算符 FixedMemoryStream& operator(val); }
【解决方案2】:

是的,有一种方法可以将必要的符号几乎降低到0:使用标准库。您的OutStream 类是std::basic_ostream 的简化版本。你的OutStream::write 真的只是std::basic_ostream::write 等等。看看here。溢出处理得非常密切,但为了完整起见,它也处理underflow,即需要数据检索;您可以将其保留为未定义(它也是 virtual)。

同样,您的FixedMemoryStreamstd::basic_streambuf&lt;T&gt;,具有固定大小(std::array&lt;T&gt;)的获取/放置区域。

所以,只要让你的类继承自标准类,你就会切断二进制大小,因为你正在重用已经声明的符号。


现在,关于template&lt;size_t bufferSize&gt; class FixedMemoryStreamWithBuffer。此类在指定和获取内存的方式方面与std::array&lt;std::uint8_t, bufferSize&gt; 非常相似。您无法对此进行太多优化:每个实例化都是一个不同类型,包含所有含义。编译器不能“合并”或对它们做任何魔术:每个实例化都必须有自己的类型。 因此,要么退回到std::vector,要么使用一些固定大小的专用块,如 32、128 等,对于介于两者之间的任何值,都会选择正确的块;这可以完全在编译时实现,因此没有运行时成本。

【讨论】:

  • 这将用标准库中的代码替换 OP 程序中的代码
  • 我的OutStream 和派生类和模板只是描述问题的一个例子。我还有其他一些案例,这些案例在标准库中没有类似的类。但是对于OutStream 的情况,我需要严格不要使用stdio,因为它会增加我的代码大小以获得相当大的字节数,并且还会从堆中分配一些内存,这在裸机上非常昂贵。跨度>
  • @Joe:您的代码是否使用(或可能使用)iostream,我的回答的第二部分仍然成立。顺便说一句,您可以尝试将 RTTI 排除在外,然后打开-nortti。 RTTI 和异常对符号和内存的影响很大。
  • 您在这里为 Alice 更换 Bob。如果他不使用 iostream,则更改他的类以使用它意味着将 iostream 编译到他的二进制文件中。在这种情况下,爱丽丝很可能比鲍勃更胖。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-10-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多