优化析构函数大小
Optimize destructors size away
我正在为嵌入式系统构建代码,我正在尝试尽可能多地保存二进制文件 space。
该代码用于解析协议(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...) {}
};
这工作得很好,可以削减很多使用过的二进制代码 space。顺便说一句,我在这里使用 final
以便编译器可以去虚拟化,并且我在没有 RTTI 的情况下进行编译(显然也使用 -Os 并且每个函数在其自己的部分中被垃圾收集)。
然而,当我检查符号 table 大小时,我发现析构函数有很多重复(所有模板实例都实现了一个明显相同(相同的二进制大小)或为空的析构函数).
通常,我知道 ControlPacket<CONNECT>
需要调用 ~FixedHeader<CONNECT>()
并且 ControlPacket<PUBLISH>
需要在销毁时调用 ~FixedHeader<PUBLISH>()
。
然而,由于所有的析构函数都是虚拟的,有没有办法让 ControlPacket
的特化避免它们的析构函数,而是让 ControlPacketBase
虚拟地析构它们,这样我就不会结束有 16 个无用的析构函数,但只有一个?
值得指出的是,这与称为 "identical COMDAT folding" 或 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),所以它在任何时候都不会通过基删除。换句话说,当您为从 [=24= 派生的某些 u
创建 shared_ptr<T>(u)
时,shared_ptr
直接存储指向 U::~U
的函数指针。
但是,这种类型擦除只会重新引入问题并生成更多空虚析构函数。请参阅 modified toy example 进行比较。我提到这个是为了完整性,以防你碰巧已经把它们放在旁边的 shared_ptr 中。
有用:另一种方法是避免生命周期管理的虚拟调度,并使用 variant
。做这样一个笼统的声明并不合适,但通常你可以实现更小的代码,甚至可以通过标签调度实现一些加速,因为你避免指定 vtables 和动态分配。
这需要对您的代码进行最大的更改,因为必须以不同的方式与代表您的数据包的对象进行交互(它不再是 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
成员函数,所以它实际上只是挑选字符串文字到 return 基于变量值。
变体的权衡是你必须有一组封闭的选项,并且你不再有一个虚拟接口来强制存在某些功能。在 return 中你得到更小的代码并且编译器通常可以看穿分派 "wall".
但是,如果我们为每个 "expected" 成员函数定义这些简单的访问者辅助函数,它就会充当界面检查器 - 而且您已经有了辅助 class 模板来保持一致.
最后,作为上述的扩展:您始终可以自由地在基础 class 中维护一些虚函数。如果您可以接受 vtables 的成本,这可以提供两全其美的方法:
#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.
我们这里有一个虚拟基础 class(但是,没有虚拟析构函数,因为它不是必需的),以及一个 to_base
函数,它可以接受变体和 return为您提供通用的基本接口。 (在像您这样的层次结构中,每种基数都可以有多个。)
您可以从公共基地自由执行虚拟调度。这有时更容易管理并且速度更快,具体取决于工作负载,并且额外的自由仅花费一些 vtables。在此示例中,我将 what
实现为首先转换为基础 class,然后对 what
成员函数执行虚拟分派。
再次强调一次访问的定义,这次在to_base
:
to_base(boost::variant<A, B> const&):
lea rax, [rdi+8]
ret
编译器理解 classes 的闭集全部继承自 Base
,因此根本不需要实际检查任何变体类型标记。
上面我用的是Boost.Variant。不是每个人都可以或想要使用 Boost,但答案的原则仍然适用:存储一个对象并跟踪整数中存储的是什么类型的对象。当需要做某事时,查看整数并跳转到代码中的正确位置。
实现变体是一个完全不同的问题。 :)
我正在为嵌入式系统构建代码,我正在尝试尽可能多地保存二进制文件 space。
该代码用于解析协议(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...) {}
};
这工作得很好,可以削减很多使用过的二进制代码 space。顺便说一句,我在这里使用 final
以便编译器可以去虚拟化,并且我在没有 RTTI 的情况下进行编译(显然也使用 -Os 并且每个函数在其自己的部分中被垃圾收集)。
然而,当我检查符号 table 大小时,我发现析构函数有很多重复(所有模板实例都实现了一个明显相同(相同的二进制大小)或为空的析构函数).
通常,我知道 ControlPacket<CONNECT>
需要调用 ~FixedHeader<CONNECT>()
并且 ControlPacket<PUBLISH>
需要在销毁时调用 ~FixedHeader<PUBLISH>()
。
然而,由于所有的析构函数都是虚拟的,有没有办法让 ControlPacket
的特化避免它们的析构函数,而是让 ControlPacketBase
虚拟地析构它们,这样我就不会结束有 16 个无用的析构函数,但只有一个?
值得指出的是,这与称为 "identical COMDAT folding" 或 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),所以它在任何时候都不会通过基删除。换句话说,当您为从 [=24= 派生的某些 u
创建 shared_ptr<T>(u)
时,shared_ptr
直接存储指向 U::~U
的函数指针。
但是,这种类型擦除只会重新引入问题并生成更多空虚析构函数。请参阅 modified toy example 进行比较。我提到这个是为了完整性,以防你碰巧已经把它们放在旁边的 shared_ptr 中。
有用:另一种方法是避免生命周期管理的虚拟调度,并使用 variant
。做这样一个笼统的声明并不合适,但通常你可以实现更小的代码,甚至可以通过标签调度实现一些加速,因为你避免指定 vtables 和动态分配。
这需要对您的代码进行最大的更改,因为必须以不同的方式与代表您的数据包的对象进行交互(它不再是 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
成员函数,所以它实际上只是挑选字符串文字到 return 基于变量值。
变体的权衡是你必须有一组封闭的选项,并且你不再有一个虚拟接口来强制存在某些功能。在 return 中你得到更小的代码并且编译器通常可以看穿分派 "wall".
但是,如果我们为每个 "expected" 成员函数定义这些简单的访问者辅助函数,它就会充当界面检查器 - 而且您已经有了辅助 class 模板来保持一致.
最后,作为上述的扩展:您始终可以自由地在基础 class 中维护一些虚函数。如果您可以接受 vtables 的成本,这可以提供两全其美的方法:
#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.
我们这里有一个虚拟基础 class(但是,没有虚拟析构函数,因为它不是必需的),以及一个 to_base
函数,它可以接受变体和 return为您提供通用的基本接口。 (在像您这样的层次结构中,每种基数都可以有多个。)
您可以从公共基地自由执行虚拟调度。这有时更容易管理并且速度更快,具体取决于工作负载,并且额外的自由仅花费一些 vtables。在此示例中,我将 what
实现为首先转换为基础 class,然后对 what
成员函数执行虚拟分派。
再次强调一次访问的定义,这次在to_base
:
to_base(boost::variant<A, B> const&):
lea rax, [rdi+8]
ret
编译器理解 classes 的闭集全部继承自 Base
,因此根本不需要实际检查任何变体类型标记。
上面我用的是Boost.Variant。不是每个人都可以或想要使用 Boost,但答案的原则仍然适用:存储一个对象并跟踪整数中存储的是什么类型的对象。当需要做某事时,查看整数并跳转到代码中的正确位置。
实现变体是一个完全不同的问题。 :)