我的问题基本上已在标题中完整陈述,但请让我详细说明。
问题:
也许值得重新措辞,virtual
方法必须有多复杂/简单,使机制成为一个相当大的开销?这有什么经验法则吗?例如。如果需要10分钟,则使用I / O,复杂if
语句,内存操作等,这不是问题。或者,如果您编写virtual get_r() { return sqrt( x*x + y*y); };
并在循环中调用它,您将遇到麻烦。
我希望这个问题不是太笼统,因为我寻求一些一般但具体的技术答案。无论是难以辨认还是不可能,或者虚拟调用需要花费大量时间/周期资源,而数学需要这样,I / O就可以了。
也许一些技术人员知道要比较的一般数字或进行一些分析,并且可以分享一般性结论。令人尴尬的是,我不知道如何制作那些花哨的asm
分析= /。
我还想提出一些理由,以及我的用例。
我认为,为了表现,人们在干旱期间避免使用像森林中的明火这样的虚拟现象,为了表现,我看到了很多问题,而且很多人都问他们“你是否绝对确定虚拟开销确实是一个问题在你的情况下?“。
在我最近的工作中,我遇到了一个问题,可以放在河的两边,我相信。
另外请记住,我不问如何改进界面的实现。我相信我知道该怎么做。我问是否有可能告诉你什么时候做,或者选择哪一个蝙蝠。使用情况:
我运行了一些模拟。我有一个基本上提供运行环境的类。有一个基类,以及一个定义一些不同工作流的派生类。 Base将东西收集为通用逻辑并分配I / O源和接收器。衍生品通过实施RunEnv::run()
或多或少来定义特定的工作流程。我认为这是一个有效的设计。现在让我们假设作为工作流主体的对象可以放在2D或3D平面中。在两种情况下,工作流都是通用的/可互换的,因此我们正在处理的对象可以具有通用接口,但是对于非常简单的方法,例如Object::get_r()
。最重要的是,我们可以为环境定义一些stat logger。
最初我想提供一些代码片段,但最终只有5个类和2-4个方法,即code
的墙。我可以根据要求发布它,但它会将问题延长到当前大小的两倍。
关键点是:RunEnv::run()
是主循环。通常很长(5分钟-5小时)。它提供基本的时间检测,调用RunEnv::process_iteration()
和RunEnv::log_stats()
。一切都是虚拟的。理由是。我可以推导出RunEnv
,例如针对不同的停止条件重新设计run()
。我可以重新设计process_iteration()
,例如使用多线程,如果我必须处理一个对象池,以各种方式处理它们。此外,不同的工作流程还需要记录不同的统计信息。 RunEnv::log_stats()
只是一个将已计算的有趣统计信息输出到std::ostream
的调用。我猜测使用虚拟,并没有真正的影响。
现在让我们假设迭代通过计算对象到原点的距离来工作。所以我们有接口double Obj::get_r();
。 Obj
是2D和3D案例的实现。在两种情况下,getter都是一个简单的数学运算,有2-3次乘法和加法。
我还尝试了不同的内存处理。例如。有时坐标数据存储在私有变量中,有时存储在共享池中,因此即使get_x()
也可以通过实现get_x(){return x;};
或get_x(){ return pool[my_num*dim+x_offset]; };
变为虚拟变量。想象一下用get_r(){ sqrt(get_x()*get_x() + get_y()*get_y()) ;};
计算一些东西。我怀疑这里的虚拟会破坏性能。
答案 0 :(得分:8)
x86上的C ++中的虚方法调用产生类似于(单继承)的代码:
mov ecx,[esp+4]
mov eax,[ecx] // pointer to vtable
jmp [eax]
如果没有虚拟,则与非虚拟成员函数相比,您将保留一条mov
指令。因此,在单继承的情况下,性能损失可以忽略不计。
如果您有多个继承,或者更糟糕的是虚拟继承,虚拟调用可能会复杂得多。但这是类层次结构和体系结构的更多问题。
经验法则
如果方法的主体比单个mov
指令慢很多倍(> 100x) - 只需使用virtual
而不要打扰。否则 - 描述您的瓶颈并进行优化。
<强>更新强>
对于多个/虚拟继承案例,请查看此页面:http://www.lrdev.com/lr/c/virtual.html
答案 1 :(得分:8)
这有什么经验法则吗?
对于像这样的问题,最好的,最常见的经验法则是:
在优化之前衡量您的代码
尝试在不进行测量的情况下使代码运行良好,这是在不同地方优化的不必要复杂代码的可靠途径。
所以,在你有一些确凿的证据证明virtual
是问题之前,不要担心虚函数的开销。如果您确实有这样的证据,那么您可以在这种情况下删除virtual
。但是,更有可能的是,您会发现找到加速计算的方法,或者避免计算您不需要的计算方法,将会产生更大的性能提升。但同样,不要只是猜测 - 先测量。
答案 2 :(得分:3)
首先,当然,任何差异都取决于编译器, 架构等等。在某些机器上,区别 虚拟呼叫和非虚拟呼叫几乎不可测量, 至少在另一方面,它将(或将 - 我的经验) 这台机器相当古老)完全吹扫管道 (没有间接跳跃的分支预测)。
在大多数处理器上,虚拟功能的实际成本是内联能力的丧失, 由此导致其他优化可能性的丧失。换句话说,成本 实际上将取决于调用函数的上下文。
然而,更重要的是:虚拟功能和非虚拟功能 函数具有不同的语义。所以你不能选择:如果你 需要虚拟语义,你必须使用虚拟;如果你不这样做 需要虚拟语义,你不能使用虚拟。所以问题 真的没有出现。答案 3 :(得分:1)
绝对最基本的建议,正如其他人所说的那样,您应该在特定的应用程序和环境中进行分析,是为了避免在紧密循环中virtual
。
请注意,如果您实际上需要多态行为,虚拟成员函数可能会比大多数替代方案更好。例外情况可能是您拥有多态但同质类型的集合(集合可以是任何多态类型,但它们都是相同的类型,无论它们碰巧是哪种类型)。然后,你可以更好地将多态行为移到循环之外。
使用经典的dumb bad-OO示例使用形状,你最好用:
// "fast" way
struct Shape {
virtual void DrawAll(Collection) = 0;
};
struct Rectangle : public Shape {
virtual void DrawAll(Collection collection) {
for (const auto& rect : collection)
do_rectangle_draw();
}
};
struct Circle : public Shape {
virtual void DrawAll(Collection collection) {
for (const auto& circle : collection)
do_circle_draw();
}
};
比更天真的版本可能是这样的:
// "slow" way
struct Shape {
virtual void DrawSelf() = 0;
void DrawAll(Collection collection) {
for (const auto& shape : collection)
shape.DrawSelf(); // virtual invocation for every item in the collection!
}
};
同样,这仅适用于集合中的同类型。如果您的Collection
可以同时包含Rectangle
和Circle
,那么您将需要在迭代期间就每个实例区分使用哪种绘图方法。虚函数可能比函数指针或switch语句更快(但确定配置文件)。
上面代码的目标是将多态行为移出循环。这并不总是可行的,但是当它成功时,它通常会达到某种程度的性能胜利。对于大量对象(例如,粒子模拟器),性能差异可能非常明显。
如果一个函数在循环内没有被调用数千次,你可能不会注意到虚函数和非虚函数之间存在任何可测量的差异。但要对其进行测试以确定是否重要。