C++ 在这种情况下,是否可以避免使用虚拟方法调用?
我有一种数据类型,必须存储在一个连续数组中,为了更新该数据,该数组被迭代。棘手的部分是,我希望能够动态更改任何对象的更新方式 这就是我到目前为止的想法:C++ 在这种情况下,是否可以避免使用虚拟方法调用?,c++,optimization,C++,Optimization,我有一种数据类型,必须存储在一个连续数组中,为了更新该数据,该数组被迭代。棘手的部分是,我希望能够动态更改任何对象的更新方式 这就是我到目前为止的想法: struct Update { virtual void operator()(Data & data) {} }; struct Data { int a, b, c; Update * update; }; struct SpecialBehavior : public Update { void
struct Update {
virtual void operator()(Data & data) {}
};
struct Data {
int a, b, c;
Update * update;
};
struct SpecialBehavior : public Update {
void operator()(Data & data) override { ... }
};
然后,我将为每个数据对象分配某种类型的更新。然后,在更新过程中,所有数据都会传递到其自己的更新函子:
for (Data & data : all)
data->update(data);
据我所知,这就是所谓的战略模式
我的问题是:有没有更有效的方法?有什么方法可以实现同样的灵活性而不需要调用虚拟方法?您可以使用函数指针
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(隐式转换为函数指针)
或者,您可以使用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;
}
}
如果您不需要开放集多态性(即,您预先知道将从更新
)派生的所有类型),您可以使用变体,如std::variant
或boost::variant
:
struct Update0 { void operator()(Data & data) { /* ... */ } };
struct Update1 { void operator()(Data & data) { /* ... */ } };
struct Update2 { void operator()(Data & data) { /* ... */ } };
这将允许您:
- 避免
函数调用的成本虚拟
- 以缓存友好的方式存储
实例数据
- 使用不同的接口或任意不同的状态更新类
另外,如果您希望只通过
操作符()(Data&)
接口允许开集多态性,则可以使用类似的方法,这基本上是对具有特定签名的函数对象的类型安全引用
struct Data {
int a, b, c;
function_view<void(Data&)> update_function;
};
虚拟函数调用的开销是多少?嗯,实施必须做两件事:
这正是两种间接记忆。通过将函数指针直接放在对象中(避免从对象中查找vtable指针),可以避免这两种方法中的一种 这样做的缺点是,它只适用于单个虚拟函数,如果添加更多,就会用函数指针填充对象,导致缓存压力更大,因此可能会降低性能。只要您只是替换一个虚拟函数,就可以再添加三个,这样您的对象就膨胀了24字节
除非确保编译器能够在编译时派生实际类型的
Update
,否则无法避免第二个内存间接寻址。而且,由于这似乎是在运行时使用虚拟函数来执行决策的全部目的,因此您运气不佳:任何试图“删除”该间接操作的尝试都会产生更差的性能
(我用引号表示“remove”,因为您当然可以避免从内存中查找函数指针。代价是您正在执行类似于
开关()
或else if()的操作。)
从对象加载的某个类型标识值上的梯形图,这将比从对象加载函数指针的成本更高。中的第二个解决方案明确地做到了这一点,而std::variant
方法通过将其隐藏在std::variant
模板中来实现。间接寻址并不是真正的remo维德,它只是隐藏在更慢的操作中。)哼。有趣。投票表决。我想你可以用C语言,用一堆函数指针来完成。使用虚拟函数真的是这里的瓶颈吗?你能仔细检查一下你的指针和箭头吗?我认为目前还不太正确。您是否确定virtual
调用的性能受到了影响?数据数组非常大,因此,即使每个对象进行很小的优化,也意味着总体上获得了很好的收益。但每个对象的成本非常小,这是对的。通常的建议是不要有一个异构集合,而是有多个异构集合。这只会删除一个间接寻址(从This
指针中查找vtable指针),如果以这种方式实现了多个virtual
函数,则会使对象膨胀。与仅声明函数virtual
相比,后一点很容易降低性能。不幸的是,开关将比函数指针更慢。我想,只要只有一个函数可以切换,您的第一个解决方案实际上是最快的方法:-)“这避免了虚拟函数的成本,但仍然有使用函数指针的成本(更少)”-请引用您的来源,或者显示证明它的基准。@cmaster:开关将比函数指针更慢=>您测量过吗?当分支预测正确时,分支成本非常低。。。直到不可见为止。@cmaster由于内联了函数,可以从data
,…中提取公共部分、常量折叠数据,因此交换机具有更大的优化潜力,…您认为std::variant
如何消除其不同类型之间的歧义?我敢打赌,它将包含一个整数,表示哪些类型当前有效。解释这样一个整数总是比vtable
虚拟调用的间接方式慢。@cmaster-oth,在缓存中使用std::vector
可能更好,因为所有数据都存储在连续内存中。在std::vector
中使用抽象类通常需要稀疏的堆分配(尽管在大多数情况下,您可能不会注意到差异)@Justin关于连续内存的观点很好。再说一次,我认为这可能更有效
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); });
}
struct Data {
int a, b, c;
function_view<void(Data&)> update_function;
};
for (Data & data : all)
{
data.update_function(data);
}