c++ - 优化析构函数的大小

标签 c++ templates compiler-optimization size-reduction

我正在为嵌入式系统构建代码,并试图节省尽可能多的二进制空间。

该代码用于解析协议(protocol)(MQTT表示其值(value)),其中有许多数据包类型,并且它们都是不同的,但是共享一些共同的部分。

目前,为了简化代码编写,我使用了以下模式:

  template <PacketType type>
  struct ControlPacket
  {
      FixedHeader<type>    type;
      VariableHeader<type> header;
      Properties<type>     props;
      ... and so on...
  };   

  // Specialize for each type
  template <>
  struct FixedHeader<CONNECT>
  {
     uint8_t typeAndFlags;
     PacketType getType() const { return static_cast<PacketType>(typeAndFlags >> 4); }
     uint8 getFlags() const { return 0; }
     bool parseType(const uint8_t * buffer, int len) 
     { 
         if (len < 1) return false; 
         typeAndFlags = buffer[0]; 
         return true; 
     }
     ...  
  };

  template <>
  struct FixedHeader<PUBLISH>
  {
     uint8_t typeAndFlags;
     PacketType getType() const { return static_cast<PacketType>(typeAndFlags >> 4); }
     uint8 getFlags() const { return typeAndFlags & 0xF; }
     bool parseType(const uint8_t * buffer, int len) 
     { 
         if (len < 1) return false; 
         typeAndFlags = buffer[0];
         if (typeAndFlags & 0x1) return false;  // Example of per packet specific check to perform
         return true; 
     }
     ...  
  };

  ... For all packet types ...

这是可行的,我现在正尝试减少所有这些模板特化的二进制影响(否则代码几乎重复了16次)

因此,我想到了这个范例:
   // Store the most common implementation in a base class
   struct FixedHeaderBase
   {
       uint8_t typeAndFlags;
       virtual PacketType getType() { return static_cast<PacketType(typeAndFlags >> 4); }
       virtual uint8 getFlags() { return 0; } // Most common code here
       virtual bool parseType(const uint8_t * buffer, int len) 
       { 
         if (len < 1) return false; 
         typeAndFlags = buffer[0]; 
         return true; 
       }

       virtual ~FixedHeaderBase() {}
   };

   // So that most class ends up empty
   template <>
   struct FixedHeader<CONNECT> final : public FixedHeaderBase
   {
   };

   // And specialize only the specific classes
   template <>
   struct FixedHeader<PUBLISH> final : public FixedHeaderBase
   {
       uint8 getFlags() const { return typeAndFlags & 0xF; }
       bool parseType(const uint8_t * buffer, int len) 
       { 
         if (!FixedHeaderBase::parseType(buffer, len)) return false; 
         if (typeAndFlags & 0x1) return false;  // Example of per packet specific check to perform
         return true; 
       }
   };

  // Most of the code is shared here
  struct ControlPacketBase
  {
     FixedHeaderBase & type;
     ...etc ...
     virtual bool parsePacket(const uint8_t * packet, int packetLen)
     {
        if (!type.parseType(packet, packetLen)) return false;
        ...etc ...
     }

     ControlPacketBase(FixedHeaderBase & type, etc...) : type(type) {} 
     virtual ~ControlPacketBase() {}
  };

  // This is only there to tell which specific version to use for the generic code
  template <PacketType type>
  struct ControlPacket final : public ControlPacketBase
  {
      FixedHeader<type>    type;
      VariableHeader<type> header;
      Properties<type>     props;
      ... and so on...

      ControlPacket() : ControlPacketBase(type, header, props, etc...) {}
  };   


这工作得很好,并且可以节省很多使用的二进制代码空间。顺便说一句,我在这里使用final以便编译器可以进行虚拟化,并且我在没有RTTI的情况下进行编译(显然也使用-Os和其自己部分中的每个函数进行了垃圾回收)。

但是,当我检查符号表的大小时,我发现析构函数上有很多重复项(所有模板实例都实现了一个明显相同(二进制大小相同)或为空的析构函数)。

通常,我知道ControlPacket<CONNECT>需要调用~FixedHeader<CONNECT>(),并且ControlPacket<PUBLISH>需要在销毁时调用~FixedHeader<PUBLISH>()

但是,由于所有析构函数都是虚拟的,是否有一种方法可以使ControlPacket的特化避免它们的析构函数,而是使用ControlPacketBase虚拟地对其进行析构,因此我不会得到16个无用的析构函数,而只能得到一个?

最佳答案

值得指出的是,这与称为“相同的COMDAT折叠”或ICF的优化有关。这是一个链接器功能,其中相同的功能(即空功能)全部合并为一个。

并非每个链接程序都支持此功能,也不是每个链接程序都愿意这样做(因为该语言表示不同的功能需要不同的地址),但是您的工具链可以具有此功能。这将是快速和容易的。

我将假设您的问题已通过toy example重现:

#include <iostream>
#include <memory>
#include <variant>

extern unsigned nondet();

struct Base {
    virtual const char* what() const = 0;

    virtual ~Base() = default;
};

struct A final : Base {
    const char* what() const override {
        return "a";
    }
};

struct B final : Base {
    const char* what() const override {
        return "b";
    }
};

std::unique_ptr<Base> parse(unsigned v) {
    if (v == 0) {
        return std::make_unique<A>();
    } else if (v == 1) {
        return std::make_unique<B>();
    } else {
        __builtin_unreachable();
    }
}

const char* what(const Base& b) {
    return b.what();  // virtual dispatch
}

const char* what(const std::unique_ptr<Base>& b) {
    return what(*b);
}

int main() {
    unsigned v = nondet();
    auto packet = parse(v);

    std::cout << what(packet) << std::endl;
}

反汇编显示A::~AB::~B都具有(多个)列表,即使它们为空且相同。这是= defaultfinal

如果删除virtual,那么这些虚假的定义就消失了,我们达到了目标-但是现在,当unique_ptr删除对象时,我们将调用未定义的行为。

我们有三种选择,可以在保持良好定义的行为的同时保持析构函数为非虚拟,其中两种是有用的,而另一种则没有。

没用:第一个选择是使用shared_ptr。之所以可行,是因为shared_ptr实际上对它的deleteer函数进行类型擦除(请参阅this question),因此它绝不会通过基址进行删除。换句话说,当您为源自shared_ptr<T>(u)的某些u制作T时,shared_ptr直接存储指向U::~U的函数指针。

但是,这种类型的擦除只是简单地重新引入了问题,并生成了更多的空虚拟析构函数。请参阅modified toy example进行比较。我提到这一点是出于完整性考虑,以防您碰巧已经将它们放到了shared_ptr中。

有用:替代方法是避免使用虚拟调度进行生命周期管理,而使用variant。进行这样的总括式声明并不是很恰当,但是通常,您可以实现较小的代码,甚至可以通过标签分发实现某些加速,因为避免指定vtable和动态分配。

这需要对代码进行最大的更改,因为代表包的对象必须以不同的方式进行交互(不再是is-a关系):
#include <iostream>

#include <boost/variant.hpp>

extern unsigned nondet();

struct Base {
    ~Base() = default;
};

struct A final : Base {
    const char* what() const {
        return "a";
    }
};

struct B final : Base {
    const char* what() const {
        return "b";
    }
};

typedef boost::variant<A, B> packet_t;

packet_t parse(unsigned v) {
    if (v == 0) {
        return A();
    } else if (v == 1) {
        return B();
    } else {
        __builtin_unreachable();
    }
}

const char* what(const packet_t& p) {
    return boost::apply_visitor([](const auto& v){
        return v.what();
    }, p);
}

int main() {
    unsigned v = nondet();
    auto packet = parse(v);

    std::cout << what(packet) << std::endl;
}

我使用Boost.Variant是因为它产生the smallest code。令人讨厌的是,std::variant坚持要生成一些次要的但存在的vtables来实现自身-我觉得这有点违背了这个目的,尽管即使使用了可变的vtables,代码总体上仍然小得多。

我想指出现代优化编译器的一个不错的结果。注意what的最终实现:
what(boost::variant<A, B> const&):
        mov     eax, DWORD PTR [rdi]
        cdq
        cmp     eax, edx
        mov     edx, OFFSET FLAT:.LC1
        mov     eax, OFFSET FLAT:.LC0
        cmove   rax, rdx
        ret

编译器了解变体中封闭的选项集,lambda鸭式打字证明每个选项确实具有...::what成员函数,因此,它实际上只是根据变体值选择要返回的字符串文字。

使用变体的权衡是,您必须具有一组封闭的选项,并且您不再具有强制存在某些功能的虚拟接口(interface)。作为返回,您可以获得较小的代码,并且编译器通常可以看到分派(dispatch)的“墙”。

但是,如果我们为每个“期望的”成员函数定义这些简单的访问者帮助程序函数,则它将充当接口(interface)检查器-此外,您已经获得了帮助程序类模板以保持一致。

最后,作为上述内容的扩展:您始终可以在基类中维护一些虚函数。如果您可以接受vtable的价格,那么这可以提供两全其美的选择:
#include <iostream>

#include <boost/variant.hpp>

extern unsigned nondet();

struct Base {
    virtual const char* what() const = 0;

    ~Base() = default;
};

struct A final : Base {
    const char* what() const override {
        return "a";
    }
};

struct B final : Base {
    const char* what() const override {
        return "b";
    }
};

typedef boost::variant<A, B> packet_t;

packet_t parse(unsigned v) {
    if (v == 0) {
        return A();
    } else if (v == 1) {
        return B();
    } else {
        __builtin_unreachable();
    }
}

const Base& to_base(const packet_t& p) {
    return *boost::apply_visitor([](const auto& v){
        return static_cast<const Base*>(&v);
    }, p);
}

const char* what(const Base& b) {
    return b.what();  // virtual dispatch
}

const char* what(const packet_t& p) {
    return what(to_base(p));
}

int main() {
    unsigned v = nondet();
    auto packet = parse(v);

    std::cout << what(packet) << std::endl;
}

This produces fairly compact code

我们这里有一个虚拟基类(但是不需要虚拟析构函数,因为它不是必需的),还有一个to_base函数,它可以采用一个变体并为您返回通用基接口(interface)。 (在像您这样的层次结构中,每种基础都可以有多个。)

在通用基础上,您可以自由执行虚拟调度。有时,这更易于管理,并且根据工作负载而更快,并且额外的自由只花费了一些vtable。在此示例中,我实现了what,首先将其转换为基类,然后对what成员函数执行虚拟分派(dispatch)。

再次,我想在to_base中指出一次访问的定义:
to_base(boost::variant<A, B> const&):
        lea     rax, [rdi+8]
        ret

编译器了解封闭类的集合,它们都是从Base继承的,因此根本不必检查任何变体类型标记。

在上面,我使用了Boost.Variant。并非每个人都可以或不想使用Boost,但答案的原理仍然适用:存储对象并跟踪整数中存储的对象类型。当需要做某事时,偷看整数并跳转到代码中的正确位置。

实现变体是一个完全不同的问题。 :)

关于c++ - 优化析构函数的大小,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60745170/

相关文章:

c# - .NET 编译器——是否内置了嵌套循环优化?

c - 条件移动优化是否针对 C 标准?

c++ - 在 gtest 类型测试中使用静态 constexpr

javascript - Angular 是否接受带有连字符的属性?

c++ - 具有 void 类型分支的三元运算符

c++ - 可变参数构造函数是否应该隐藏隐式生成的构造函数?

c++ - 类模板,在定义中引用它自己的类型

c++ - 等量的构造函数和析构函数调用是否确保没有内存泄漏?

c++ - 递归链表差异

c++ - QMutexLocker 和 QMutex 哪个更好用?