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

Ano*_*ity 19 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 { ... }
};
Run Code Online (Sandbox Code Playgroud)

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

for (Data & data : all)
   data->update(data);
Run Code Online (Sandbox Code Playgroud)

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

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

cma*_*ter 16

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

  1. 从对象加载vtable指针.
  2. 从vtable加载函数指针.

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

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


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

(我说"删除"加上引号,因为你肯定能避免查找从内存中的函数指针,价格将是您要执行类似一个switch()else if()某些类型的标识值梯子从对象加载,这将变成是比仅仅从对象加载函数指针更昂贵.ralismarks的第二个解决方案明确地做了这个,而Vittorio Romeostd::variant<>方法将它隐藏在模板中.间接并没有真正被删除,它只是隐藏在更慢的操作中.)std::variant<>

  • 我的想法完全正确 tl; dr:只使用虚函数. (5认同)
  • 实际上,您错过了最大的开销:无法内联函数的机会成本. (2认同)

ral*_*ark 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;
Run Code Online (Sandbox Code Playgroud)

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

a.update = [](Data & data) { ... };
Run Code Online (Sandbox Code Playgroud)

或者,您可以使用enum和switch语句.

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

  • "这可以避免虚拟功能的成本,但仍然需要使用功能指针(更少)的成本" - 请引用您的来源,或显示证明它的基准. (5认同)
  • 这只会删除一个间接(从'this`指针查找vtable指针),如果以这种方式实现多个`virtual`函数,则会使对象膨胀.与仅声明函数`virtual`相比,后一点可以轻松*降低*性能. (4认同)
  • @cmaster交换机具有更多优化潜力,因为内联函数可以提取公共部分,从"数据"中提取常量折叠数据,... (4认同)
  • 不幸的是,`switch`甚至比函数指针慢.我想,只要只有一个功能可以切换,你的第一个解决方案实际上是最快的方法:-) (3认同)
  • @cmaster:*开关甚至比功能指针慢*=>你测量过吗?当分支预测是正确的时,分支成本*实际上真的很小......直到看不见. (2认同)

Vit*_*meo 5

如果你不需要开集多态 (即你事先知道所有可以派生的类型Update),你可以使用像或的变体:std::variantboost::variant

struct Update0 { void operator()(Data & data) { /* ... */ } };
struct Update1 { void operator()(Data & data) { /* ... */ } };
struct Update2 { void operator()(Data & data) { /* ... */ } };
Run Code Online (Sandbox Code Playgroud)

struct Data {
    int a, b, c;
    std::variant<Update0, Update1, Update2> update;
};
Run Code Online (Sandbox Code Playgroud)

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

这将允许您:

  • 避免virtual函数调用的成本.

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

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


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

struct Data {
    int a, b, c;
    function_view<void(Data&)> update_function;
};
Run Code Online (Sandbox Code Playgroud)

for (Data & data : all)
{
    data.update_function(data);
}
Run Code Online (Sandbox Code Playgroud)

  • @cmaster OTOH,`std :: vector <std :: variant <>>`可能在缓存上更好,因为所有数据都存储在连续的内存中.在`std :: vector`中使用抽象类通常需要可能稀疏的堆分配(尽管在大多数情况下,您可能不会注意到差异) (2认同)