我正在为嵌入式系统构建代码,并试图节省尽可能多的二进制空间。
该代码用于解析协议(MQTT代表其价值),其中包类型很多,并且它们都是不同的,但是共享一些共同的部分。
当前,为了简化代码编写,我正在使用此模式:
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::~A
和B::~B
都具有(多个)列表,即使它们为空且相同。这是= default
和final
。
如果删除virtual
,那么这些虚假的定义就消失了,我们达到了目标-但是现在,当unique_ptr删除对象时,我们将调用未定义的行为。
我们有三种选择,可以在保持良好定义的行为的同时保持析构函数为非虚拟,其中两个有用,一个没有用。
无用:第一个选择是使用shared_ptr
。之所以行之有效,是因为shared_ptr
实际上是按类型擦除其删除函数(请参见this question),因此它绝不会通过基数删除。换句话说,当您为源自shared_ptr<T>(u)
的某些u
制作T
时,shared_ptr
将直接存储指向U::~U
的功能指针。
但是,这种类型的擦除只是简单地重新引入了问题,并生成了更多的空虚拟析构函数。参见modified toy example进行比较。出于完整性考虑,我在此提及,以防您碰巧已经将它们放在shared_ptr的一侧。
有用:替代方法是避免虚拟分发进行生命周期管理,而使用variant
。进行这样的总括式声明并不是很适当,但是通常
这需要对代码进行最大的更改,因为代表包的对象必须以不同的方式进行交互(不再是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
成员函数,因此,它实际上只是根据变体值挑选字符串文字以返回。与变体的权衡是,您必须具有一组封闭的选项,并且您不再具有强制存在某些功能的虚拟接口。作为回报,您将获得较小的代码,并且编译器通常可以看到调度“墙”。
但是,如果我们为每个“预期”成员函数定义这些简单的访问者帮助程序函数,它将充当接口检查器-另外,您已经获得了帮助程序类模板以保持一致。
最后,作为上述的扩展:您始终可以在基类中维护某些虚函数。如果您可以接受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
函数,它可以采用一个变体并为您返回通用基接口。 (在像您这样的层次结构中,每种基础都可以有多个。)
从通用库中您可以自由执行虚拟调度。有时,这更易于管理,并且根据工作负载而更快,并且额外的自由只花费了一些vtable。在此示例中,我实现了what
,首先将其转换为基类,然后对what
成员函数执行虚拟分派。
再次,我想指出一次访问的定义,这次是在to_base
:
to_base(boost::variant<A, B> const&): lea rax, [rdi+8] ret
编译器了解封闭的类集,它们都是从
Base
继承的,因此根本不必检查任何变体类型标记。在上面,我使用了Boost.Variant。并不是每个人都可以或不想使用Boost,但是答案的原理仍然适用:存储对象并跟踪整数中存储的对象类型。当需要执行某些操作时,请查看整数并跳转到代码中的正确位置。
实施变体是一个完全不同的问题。 :)