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_snapshotsaveload基本上是函数指针,不需要单独赋值class!与

struct PersistentAPI {
    std::function<void(int)> take_snapshot;
    std::function<void(int)> save;
    std::function<void(int)> load;
};

,作为开发人员,完全有理由相信 PersistentAPI 意味着 分派到多个对象,而不仅仅是 *单个对象*。例如,take_snapshot 可以调度到一个自由函数,而 saveload 可以调度到两个不同的 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.