在这种情况下是否可以避免使用虚方法调用?

时间:2017-04-21 10:14:16

标签: c++ optimization

我有一种必须存储在连续数组中的数据,为了更新该数据,迭代数组被重复。棘手的部分是我希望能够动态地改变任何对象的更新方式。

这是我到目前为止所提出的:

struct Update {
    virtual void operator()(Data & data) {}
};

struct Data {
    int a, b, c;
    Update * update;
};

struct SpecialBehavior : public Update {
    void operator()(Data & data) override { ... }
};

然后我会为每个数据对象分配一些类型的Update。然后在更新期间,所有数据都会传递给它自己的更新函数:

for (Data & data : all)
   data->update(data);

据我所知,这就是战略模式。

我的问题:有没有办法更有效地做到这一点?有一些方法可以实现相同的灵活性而无需调用虚方法的成本吗?

3 个答案:

答案 0 :(得分:16)

虚拟函数调用的开销是多少?那么,实现必须做两件事:

  1. 从对象加载vtable指针。
  2. 从vtable加载函数指针。
  3. 这恰恰是两个记忆间接。您可以通过将函数指针直接放在对象中来避免两者中的一个(避免从对象中查找vtable指针),这是ralismarks answer给出的方法。

    这有一个缺点,它只适用于单个虚函数,如果你添加更多,你将使用函数指针膨胀你的对象,导致你的缓存压力更大,因此可能会降低性能。只要您只是替换单个虚函数,那就没问题,再添加三个,并且您的对象膨胀了24个字节。

    除非确保编译器可以在编译时派生Update的实际类型,否则无法避免第二个内存间接。而且由于似乎是使用虚函数在运行时执行决策的重点,所以你运气不好:任何“删除”间接的尝试都会产生更差的性能。

    (我在引号中说“删除”,因为你当然可以避免从内存中查找一个函数指针。价格是你在某种类型上执行类似switch()else if()梯形的东西识别从对象加载的值,这将比仅从对象加载函数指针更昂贵。ralismarks answer中的第二个解决方案明确地执行此操作,而{{1 Vittorio Romeo方法隐藏它在std::variant<>模板中。间接不会被删除,它只是隐藏在更慢的操作中。)

答案 1 :(得分:10)

您可以改为使用函数指针。

struct Data;

using Update = void (*)(Data &);

void DefaultUpdate(Data & data) {};

struct Data {
    int a, b, c;
    Update update = DefaultUpdate;
};

void SpecialBehavior(Data & data) { ... };
// ...
Data a;
a.update = &SpecialBehaviour;

这避免了虚函数的成本,但仍然具有使用函数指针(较少)的成本。从C ++ 11开始,你也可以使用非捕获lambda(可以隐式转换为函数指针)。

a.update = [](Data & data) { ... };

或者,您可以使用枚举和开关语句。

enum class UpdateType {
    Default,
    Special
};

struct Data {
    int a, b, c;
    UpdateType behavior;
};

void Update(Data & data) {
    switch(data.behavior) {
        case UpdateType::Default:
            DoThis(data);
            break;
        case UpdateType::Special:
            DoThat(data);
            break;
    }
}

答案 2 :(得分:5)

如果您不需要 open-set polymorphism (即事先知道所有类型将来自Update,您可以使用<强>变体,例如std::variantboost::variant

struct Update0 { void operator()(Data & data) { /* ... */ } };
struct Update1 { void operator()(Data & data) { /* ... */ } };
struct Update2 { void operator()(Data & data) { /* ... */ } };

struct Data {
    int a, b, c;
    std::variant<Update0, Update1, Update2> update;
};

for (Data & data : all)
{
    std::visit(data.update, [&data](auto& x){ x(data); });
}

这将允许您:

  • 避免 virtual函数调用的费用

  • 以缓存友好的方式存储您的Data个实例。

  • Update个类具有不同的接口或任意不同的状态。

另外,如果你想允许 open-set polymorphism 但只能通过operator()(Data&)接口,你可以使用类似function_view的东西,这基本上是一种类型 - 安全引用具有特定签名的功能对象

struct Data {
    int a, b, c;
    function_view<void(Data&)> update_function;
};

for (Data & data : all)
{
    data.update_function(data);
}