std::function 的开销与类型擦除的虚函数调用
Overhead of std::function vs virtual function call for type erasure
假设我有一个模板化的 class,它包装了它的模板参数以提供一些额外的功能,比如将对象的状态持久保存到磁盘的能力:
template<typename T>
class Persistent {
std::unique_ptr<T> wrapped_obj;
public:
Persistent(std::unique_ptr<T> obj_to_wrap);
void take_snapshot(int version);
void save(int to_version);
void load(int to_version);
}
我想要另一个 class,我们称它为 PersistentManager,存储这些模板化 Persistent 对象的列表,并在 不知道它们的模板参数的情况下调用它们的成员方法。我可以看到有两种方法可以做到这一点:使用 std::function 从每个方法中删除模板类型,或者使用抽象基础 class 和虚函数调用。
使用std::function、,每个持久对象都能够返回绑定到其成员的 std::function 束:
struct PersistentAPI {
std::function<void(int)> take_snapshot;
std::function<void(int)> save;
std::function<void(int)> load;
}
template<typename T>
PersistentAPI Persistent<T>::make_api() {
using namespace std::placeholders;
return {std::bind(&Persistent<T>::take_snapshot, this, _1),
std::bind(&Persistent<T>::save, this, _1),
std::bind(&Persistent<T>::load, this, _1)}
}
然后 PersistentManager 可以存储 PersistentAPI
的列表,并且有这样的方法:
void PersistentManager::save_all(int version) {
for(PersistentAPI& bundle : persistents) {
bundle.save(version);
}
}
使用继承, 我会创建一个没有模板参数的抽象 class,将每个 Persistent 的方法定义为虚拟的,并使 Persistent 从它继承。然后 PersistentManager 可以存储指向这个基的指针 class,并通过虚函数调用来调用 Persistent 方法:
class AbstractPersistent {
public:
virtual void take_snapshot(int version) = 0;
virtual void save(int to_version) = 0;
virtual void load(int to_version) = 0;
}
template<typename T>
class Persistent : public AbstractPersistent {
...
}
void PersistentManager::save_all(int version) {
for(AbstractPersistent* obj : persistents) {
obj->save(version);
}
}
这两种方法都为 PersistentManager 的函数调用增加了一些开销:它们不是将函数调用直接分派给 Persistent 实例,而是需要通过一个中间层,即 std::function 对象或虚拟对象AbstractPersistent.
中的函数 table
我的问题是,哪种方法增加的开销少?由于这些都是标准库中相当不透明的部分,我不太清楚 std::function 调用与通过基 class 指针调用虚函数相比有多“昂贵”。
(我在本网站上发现了一些其他问题,询问 std::function 的开销,但它们都缺乏具体的替代方案来进行比较。)
我有点犹豫要不要回答这个问题,因为它很容易归结为意见。我一直在项目中使用 std::function
,所以我不妨分享我的两分钱(您可以决定如何处理输入)。
首先,我想重复一下评论中已经说过的话。如果你真的想看到性能,你必须做一些基准测试。只有对标后,你才能得出结论。
幸运的是,您可以使用 quick-bench 进行快速基准测试 (!)。我用你的两个版本提供了基准测试,添加了每次调用都会增加的状态,并为变量添加了 getter:
// Type erasure:
struct PersistentAPI {
std::function<void(int)> take_snapshot;
std::function<void(int)> save;
std::function<void(int)> load;
std::function<int()> get;
};
// Virtual base class
class AbstractPersistent {
public:
virtual void take_snapshot(int version) = 0;
virtual void save(int to_version) = 0;
virtual void load(int to_version) = 0;
virtual int get() = 0;
};
每个函数只是在对应的class中增加一个整数,returns它与get()
(希望编译器不要删除所有不必要的代码)。
结果有利于虚函数,对于 Clang 和 GCC,我们有大约 1.7 的速度差异(https://quick-bench.com/q/wUbPp8OdtzLZv8H1VylyuDnd2pU,您可以更改编译器并重新检查)。
现在开始分析:为什么 摘要class 看起来更快?好吧,std::function
有更多的间接寻址,但在我们调用 std::bind
(!) 之前的包装中也有另一个间接寻址。听 Scott Meyers 说,lambda 比 std::bind
更受欢迎,不仅是因为它们对人们来说语法简单(std::placeholders
并不漂亮),而且它们对编译器来说也是语法! lambda 调用更容易到内联。
内联对于性能非常重要。如果可以通过在我们调用的地方添加代码来避免显式调用,我们可以节省一些周期!
将 std::bind
更改为 lambda,并再次执行,我们在 std::function 和继承(对于 Clang 和 GCC)之间有非常相似的性能:https://quick-bench.com/q/HypCbzz5UMo1aHtRpRbrc9B8v44.
那么,为什么它们相似?对于 Clang 和 GCC,std::function
是 内部 使用继承。此处实现的类型擦除只是 隐藏 多态性。
(请注意,此基准测试可能会产生误导,因为两种情况的调用都可以完全内联,因此根本不使用间接寻址。测试用例 可能 必须是欺骗编译器有点棘手。)
假设您有 Clang 和 GCC 作为编译器,您应该使用哪种方法?
持久API更灵活,因为实际上take_snapshot
、save
和load
基本上是函数指针,不需要单独赋值class!与
struct PersistentAPI {
std::function<void(int)> take_snapshot;
std::function<void(int)> save;
std::function<void(int)> load;
};
,作为开发人员,完全有理由相信 PersistentAPI
是 意味着 分派到多个对象,而不仅仅是 *单个对象*。例如,take_snapshot
可以调度到一个自由函数,而 save
和 load
可以调度到两个不同的 classes。这是您想要的灵活性吗?那就是你应该使用的。通常,我会使用 std::function
到 API 让用户注册一个 回调 到任何选择的回调。
如果您想使用类型擦除,但出于某种原因想要隐藏继承,您可以构建自己的版本。 std::function
接受具有 operator()
的所有类型,我们可以构建一个接受具有接口“take_snapshot,保存和加载”的所有 classes 的类型。好好练练!
// probably there is a better name for this class
class PersistentTypeErased {
public:
template<typename T>
PersistentTypeErased(T t) : t_(std::make_unique<Model<T>>(t)) {}
void take_snapshot(int version) { t_->take_snapshot(version); }
void save(int to_version) { t_->save(to_version); }
void load(int to_version) { t_->load(to_version); }
private:
struct Concept
{
virtual void take_snapshot(int version) = 0;
virtual void save(int to_version) = 0;
virtual void load(int to_version) = 0;
};
template<typename T>
struct Model : Concept
{
Model(T t) : t_(t) {}
void take_snapshot(int version) { t_.take_snapshot(version); }
void save(int to_version) { t_.save(to_version); }
void load(int to_version) { t_.load(to_version); }
T t_;
};
std::unique_ptr<Concept> t_;
};
该技术类似于 std::function
,现在您可能还可以了解类型擦除如何在幕后使用多态性。你可以看看它是如何使用的 here.
假设我有一个模板化的 class,它包装了它的模板参数以提供一些额外的功能,比如将对象的状态持久保存到磁盘的能力:
template<typename T>
class Persistent {
std::unique_ptr<T> wrapped_obj;
public:
Persistent(std::unique_ptr<T> obj_to_wrap);
void take_snapshot(int version);
void save(int to_version);
void load(int to_version);
}
我想要另一个 class,我们称它为 PersistentManager,存储这些模板化 Persistent 对象的列表,并在 不知道它们的模板参数的情况下调用它们的成员方法。我可以看到有两种方法可以做到这一点:使用 std::function 从每个方法中删除模板类型,或者使用抽象基础 class 和虚函数调用。
使用std::function、,每个持久对象都能够返回绑定到其成员的 std::function 束:
struct PersistentAPI {
std::function<void(int)> take_snapshot;
std::function<void(int)> save;
std::function<void(int)> load;
}
template<typename T>
PersistentAPI Persistent<T>::make_api() {
using namespace std::placeholders;
return {std::bind(&Persistent<T>::take_snapshot, this, _1),
std::bind(&Persistent<T>::save, this, _1),
std::bind(&Persistent<T>::load, this, _1)}
}
然后 PersistentManager 可以存储 PersistentAPI
的列表,并且有这样的方法:
void PersistentManager::save_all(int version) {
for(PersistentAPI& bundle : persistents) {
bundle.save(version);
}
}
使用继承, 我会创建一个没有模板参数的抽象 class,将每个 Persistent 的方法定义为虚拟的,并使 Persistent 从它继承。然后 PersistentManager 可以存储指向这个基的指针 class,并通过虚函数调用来调用 Persistent 方法:
class AbstractPersistent {
public:
virtual void take_snapshot(int version) = 0;
virtual void save(int to_version) = 0;
virtual void load(int to_version) = 0;
}
template<typename T>
class Persistent : public AbstractPersistent {
...
}
void PersistentManager::save_all(int version) {
for(AbstractPersistent* obj : persistents) {
obj->save(version);
}
}
这两种方法都为 PersistentManager 的函数调用增加了一些开销:它们不是将函数调用直接分派给 Persistent 实例,而是需要通过一个中间层,即 std::function 对象或虚拟对象AbstractPersistent.
中的函数 table我的问题是,哪种方法增加的开销少?由于这些都是标准库中相当不透明的部分,我不太清楚 std::function 调用与通过基 class 指针调用虚函数相比有多“昂贵”。
(我在本网站上发现了一些其他问题,询问 std::function 的开销,但它们都缺乏具体的替代方案来进行比较。)
我有点犹豫要不要回答这个问题,因为它很容易归结为意见。我一直在项目中使用 std::function
,所以我不妨分享我的两分钱(您可以决定如何处理输入)。
首先,我想重复一下评论中已经说过的话。如果你真的想看到性能,你必须做一些基准测试。只有对标后,你才能得出结论。
幸运的是,您可以使用 quick-bench 进行快速基准测试 (!)。我用你的两个版本提供了基准测试,添加了每次调用都会增加的状态,并为变量添加了 getter:
// Type erasure:
struct PersistentAPI {
std::function<void(int)> take_snapshot;
std::function<void(int)> save;
std::function<void(int)> load;
std::function<int()> get;
};
// Virtual base class
class AbstractPersistent {
public:
virtual void take_snapshot(int version) = 0;
virtual void save(int to_version) = 0;
virtual void load(int to_version) = 0;
virtual int get() = 0;
};
每个函数只是在对应的class中增加一个整数,returns它与get()
(希望编译器不要删除所有不必要的代码)。
结果有利于虚函数,对于 Clang 和 GCC,我们有大约 1.7 的速度差异(https://quick-bench.com/q/wUbPp8OdtzLZv8H1VylyuDnd2pU,您可以更改编译器并重新检查)。
现在开始分析:为什么 摘要class 看起来更快?好吧,std::function
有更多的间接寻址,但在我们调用 std::bind
(!) 之前的包装中也有另一个间接寻址。听 Scott Meyers 说,lambda 比 std::bind
更受欢迎,不仅是因为它们对人们来说语法简单(std::placeholders
并不漂亮),而且它们对编译器来说也是语法! lambda 调用更容易到内联。
内联对于性能非常重要。如果可以通过在我们调用的地方添加代码来避免显式调用,我们可以节省一些周期!
将 std::bind
更改为 lambda,并再次执行,我们在 std::function 和继承(对于 Clang 和 GCC)之间有非常相似的性能:https://quick-bench.com/q/HypCbzz5UMo1aHtRpRbrc9B8v44.
那么,为什么它们相似?对于 Clang 和 GCC,std::function
是 内部 使用继承。此处实现的类型擦除只是 隐藏 多态性。
(请注意,此基准测试可能会产生误导,因为两种情况的调用都可以完全内联,因此根本不使用间接寻址。测试用例 可能 必须是欺骗编译器有点棘手。)
假设您有 Clang 和 GCC 作为编译器,您应该使用哪种方法?
持久API更灵活,因为实际上take_snapshot
、save
和load
基本上是函数指针,不需要单独赋值class!与
struct PersistentAPI {
std::function<void(int)> take_snapshot;
std::function<void(int)> save;
std::function<void(int)> load;
};
,作为开发人员,完全有理由相信 PersistentAPI
是 意味着 分派到多个对象,而不仅仅是 *单个对象*。例如,take_snapshot
可以调度到一个自由函数,而 save
和 load
可以调度到两个不同的 classes。这是您想要的灵活性吗?那就是你应该使用的。通常,我会使用 std::function
到 API 让用户注册一个 回调 到任何选择的回调。
如果您想使用类型擦除,但出于某种原因想要隐藏继承,您可以构建自己的版本。 std::function
接受具有 operator()
的所有类型,我们可以构建一个接受具有接口“take_snapshot,保存和加载”的所有 classes 的类型。好好练练!
// probably there is a better name for this class
class PersistentTypeErased {
public:
template<typename T>
PersistentTypeErased(T t) : t_(std::make_unique<Model<T>>(t)) {}
void take_snapshot(int version) { t_->take_snapshot(version); }
void save(int to_version) { t_->save(to_version); }
void load(int to_version) { t_->load(to_version); }
private:
struct Concept
{
virtual void take_snapshot(int version) = 0;
virtual void save(int to_version) = 0;
virtual void load(int to_version) = 0;
};
template<typename T>
struct Model : Concept
{
Model(T t) : t_(t) {}
void take_snapshot(int version) { t_.take_snapshot(version); }
void save(int to_version) { t_.save(to_version); }
void load(int to_version) { t_.load(to_version); }
T t_;
};
std::unique_ptr<Concept> t_;
};
该技术类似于 std::function
,现在您可能还可以了解类型擦除如何在幕后使用多态性。你可以看看它是如何使用的 here.