作者:数到我答应我937 | 来源:互联网 | 2023-02-06 06:02
我有一种数据类型必须存储在一个连续的数组中,为了更新这些数据,迭代的数组被迭代.棘手的部分是我希望有可能动态地改变任何对象的更新方式.
这是我到目前为止所提出的:
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);
据我所知,这就是战略模式.
我的问题:有没有办法更有效地做到这一点?某些方法可以实现相同的灵活性而无需调用虚拟方法的成本?
1> cmaster..:
虚函数调用的开销是多少?那么,实现必须做两件事:
从对象加载vtable指针.
从vtable加载函数指针.
这恰恰是两个记忆的间接.您可以通过将函数指针直接放在对象中来避免两者中的一个(避免从对象中查找vtable指针),这是ralismarks answer给出的方法.
这有一个缺点,它只适用于单个虚函数,如果你添加更多,你将使用函数指针膨胀你的对象,导致你的缓存压力更大,因此可能会降低性能.只要你只是替换一个虚函数,那就没关系,再添加三个,你的对象膨胀了24个字节.
除非确保编译器可以Update
在编译时派生实际类型,否则无法避免第二个内存间接.而且由于似乎是使用虚函数在运行时执行决策的重点,所以你运气不好:任何"删除"间接的尝试都会产生更糟糕的性能.
(我说"删除"加上引号,因为你肯定能避免查找从内存中的函数指针,价格将是您要执行类似一个switch()
或else if()
某些类型的标识值梯子从对象加载,这将变成是比仅仅从对象加载函数指针更昂贵.ralismarks的第二个解决方案明确地做了这个,而Vittorio Romeo的std::variant<>
方法将它隐藏在模板中.间接并没有真正被删除,它只是隐藏在更慢的操作中.)std::variant<>
我的想法完全正确 tl; dr:只使用虚函数.
实际上,您错过了最大的开销:无法内联函数的机会成本.
2> ralismark..:
您可以使用函数指针代替.
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开始,您还可以使用非捕获lambdas(可以隐式转换为函数指针).
a.update = [](Data & data) { ... };
或者,您可以使用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;
}
}
"这可以避免虚拟功能的成本,但仍然需要使用功能指针(更少)的成本" - 请引用您的来源,或显示证明它的基准.
这只会删除一个间接(从&#39;this`指针查找vtable指针),如果以这种方式实现多个`virtual`函数,则会使对象膨胀.与仅声明函数`virtual`相比,后一点可以轻松*降低*性能.
@cmaster交换机具有更多优化潜力,因为内联函数可以提取公共部分,从"数据"中提取常量折叠数据,...
不幸的是,`switch`甚至比函数指针慢.我想,只要只有一个功能可以切换,你的第一个解决方案实际上是最快的方法:-)
@cmaster:*开关甚至比功能指针慢*=>你测量过吗?当分支预测是正确的时,分支成本*实际上真的很小......直到看不见.
3> Vittorio Rom..:
如果你不需要开集多态 (即你事先知道所有可以派生的类型Update
),你可以使用像或的变体:std::variant
boost::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 update;
};
for (Data & data : all)
{
std::visit(data.update, [&data](auto& x){ x(data); });
}
这将允许您:
避免virtual
函数调用的成本.
Data
以缓存友好的方式存储您的实例.
具有Update
不同接口或任意不同状态的类.
另外,如果你想允许open-set多态但只允许通过operator()(Data&)
接口,你可以使用类似的东西function_view
,它基本上是对具有特定签名的函数对象的类型安全引用.
struct Data {
int a, b, c;
function_view update_function;
};
for (Data & data : all)
{
data.update_function(data);
}
@cmaster OTOH,`std :: vector
>`可能在缓存上更好,因为所有数据都存储在连续的内存中.在`std :: vector`中使用抽象类通常需要可能稀疏的堆分配(尽管在大多数情况下,您可能不会注意到差异)