我有一个可以简化为这样的C ++应用程序:
class AbstractWidget {
public:
virtual ~AbstractWidget() {}
virtual void foo() {}
virtual void bar() {}
// (other virtual methods)
};
class WidgetCollection {
private:
vector<AbstractWidget*> widgets;
public:
void addWidget(AbstractWidget* widget) {
widgets.push_back(widget);
}
void fooAll() {
for (unsigned int i = 0; i < widgets.size(); i++) {
widgets[i]->foo();
}
}
void barAll() {
for (unsigned int i = 0; i < widgets.size(); i++) {
widgets[i]->bar();
}
}
// (other *All() methods)
};
我的应用程序对性能至关重要。集合中通常有数千个小部件。从AbstractWidget
派生的类(其中有几十个)通常会使许多虚函数不被覆盖。被覆盖的通常具有非常快的实现。
鉴于此,我觉得我可以通过一些聪明的元编程来优化我的系统。目标是利用函数内联并避免虚函数调用,同时保持代码可管理。我查看了奇怪的重复模板模式(有关说明,请参阅here)。这似乎几乎做我想做的事,但并不完全。
有没有办法让CRTP在这里工作?或者,还有其他任何人都能想到的聪明解决方案吗?
答案 0 :(得分:7)
模拟动态绑定(还有CRTP的其他用途)适用于基类认为自身是多态的,但客户端实际上只关心一个特定的派生类类。因此,例如,您可能拥有表示某些特定于平台的功能的接口的类,并且任何给定的平台将只需要一个实现。模式的要点是对基类进行模板化,这样即使有多个派生类,基类也会在编译时知道哪一个正在使用。
当您真正需要运行时多态时,它没有帮助,例如当您有一个AbstractWidget*
容器时,每个元素可以是几个派生类之一,并且您必须迭代它们。在CRTP(或任何模板代码)中,base<derived1>
和base<derived2>
是不相关的类。因此derived1
和derived2
也是如此。它们之间没有动态多态,除非它们有另一个公共基类,但是你回到虚拟调用的起点。
通过用几个向量替换向量可能会获得一些加速:一个用于您知道的每个派生类,另一个用于稍后添加新派生类并且不更新容器的泛型。然后addWidget执行一些(慢)typeid
检查或对窗口小部件的虚拟调用,将窗口小部件添加到正确的容器,并且可能在调用者知道运行时类时有一些重载。注意不要在WidgetIKnowAbout
向量中意外添加WidgetIKnowAbout*
的子类。 fooAll
和barAll
可以循环遍历每个容器,然后对非虚拟fooImpl
和barImpl
函数进行(快速)调用,然后进行内联。然后,他们遍历希望小得多的AbstractWidget*
向量,调用虚拟foo
或bar
函数。
它有点乱,而不是纯粹的OO,但如果几乎所有小部件都属于容器所知的类,那么你可能会看到性能提升。
请注意,如果大多数小部件属于您的容器无法知道的类(例如,因为它们位于不同的库中),那么您不可能具有内联(除非您的动态链接器可以内联。我可以' T)。您可以通过弄乱成员函数指针来降低虚拟调用开销,但增益几乎肯定可以忽略不计甚至是负数。虚拟调用的大部分开销都在调用本身,而不是虚拟查找,并且不会内联通过函数指针调用。
另一种方式:如果要内联代码,这意味着不同类型的实际机器代码必须不同。这意味着您需要多个循环或带有开关的循环,因为根据从集合中拉出的某个指针的类型,机器代码在每次通过循环时都无法在ROM中更改。/ p>
好吧,我想这个对象可能包含一些asm代码,循环复制到RAM中,标记可执行文件并跳转到。但那不是C ++成员函数。它不能轻松完成。它可能甚至不会很快,复制和icache失效的东西。这就是存在虚拟呼叫的原因......
答案 1 :(得分:5)
CRTP或编译时多态性适用于在编译时知道所有类型的情况。只要您在运行时使用addWidget
来收集小部件列表,并且只要fooAll
和barAll
然后必须在运行时处理该同类小部件列表的成员,必须能够在运行时处理不同的类型。因此,对于您提出的问题,我认为您使用的是运行时多态性。
当然,标准答案是在尝试避免运行时多态性之前验证运行时多态性的性能是否存在...
如果您确实需要避免运行时多态性,则可以使用以下解决方案之一。
选项1:使用小部件的编译时集合
如果您的WidgetCollection成员在编译时已知,那么您可以非常轻松地使用模板。
template<typename F>
void WidgetCollection(F functor)
{
functor(widgetA);
functor(widgetB);
functor(widgetC);
}
// Make Foo a functor that's specialized as needed, then...
void FooAll()
{
WidgetCollection(Foo);
}
选项2:将运行时多态性替换为自由函数
class AbstractWidget {
public:
virtual AbstractWidget() {}
// (other virtual methods)
};
class WidgetCollection {
private:
vector<AbstractWidget*> defaultFooableWidgets;
vector<AbstractWidget*> customFooableWidgets1;
vector<AbstractWidget*> customFooableWidgets2;
public:
void addWidget(AbstractWidget* widget) {
// decide which FooableWidgets list to push widget onto
}
void fooAll() {
for (unsigned int i = 0; i < defaultFooableWidgets.size(); i++) {
defaultFoo(defaultFooableWidgets[i]);
}
for (unsigned int i = 0; i < customFooableWidgets1.size(); i++) {
customFoo1(customFooableWidgets1[i]);
}
for (unsigned int i = 0; i < customFooableWidgets2.size(); i++) {
customFoo2(customFooableWidgets2[i]);
}
}
};
丑陋,真的不是OO。模板可以通过减少列出每个特殊情况的需要来帮助解决这个问题;尝试类似下面的内容(完全未经测试),但在这种情况下你回到没有内联。
class AbstractWidget {
public:
virtual AbstractWidget() {}
};
class WidgetCollection {
private:
map<void(AbstractWidget*), vector<AbstractWidget*> > fooWidgets;
public:
template<typename T>
void addWidget(T* widget) {
fooWidgets[TemplateSpecializationFunctionGivingWhichFooToUse<widget>()].push_back(widget);
}
void fooAll() {
for (map<void(AbstractWidget*), vector<AbstractWidget*> >::const_iterator i = fooWidgets.begin(); i != fooWidgets.end(); i++) {
for (unsigned int j = 0; j < i->second.size(); j++) {
(*i->first)(i->second[j]);
}
}
}
};
选项3:消除OO
OO非常有用,因为它有助于管理复杂性,因为它有助于在面对变化时保持稳定性。对于您似乎在描述的情况 - 成千上万的小部件,其行为通常不会改变,并且其成员方法非常简单 - 您可能没有太多的复杂性或更改来管理。如果是这种情况,那么您可能不需要OO。
此解决方案与运行时多态性相同,只是它要求您维护“虚拟”方法和已知子类(不是OO)的静态列表,并且它允许您使用跳转表替换虚拟函数调用以进行内联功能
class AbstractWidget {
public:
enum WidgetType { CONCRETE_1, CONCRETE_2 };
WidgetType type;
};
class WidgetCollection {
private:
vector<AbstractWidget*> mWidgets;
public:
void addWidget(AbstractWidget* widget) {
widgets.push_back(widget);
}
void fooAll() {
for (unsigned int i = 0; i < widgets.size(); i++) {
switch(widgets[i]->type) {
// insert handling (such as calls to inline free functions) here
}
}
}
};
答案 2 :(得分:4)
简短的回答是否定的。
答案很长(或者仍然是其他一些答案: - )
您正在动态地尝试确定在运行时要执行的函数(即虚函数是什么)。如果你有一个向量(由于在编译时无法确定成员),那么无论你尝试什么,你都无法弄清楚如何内联函数。
唯一的问题是,如果向量总是包含相同的元素(即,您可以计算出将在运行时执行的编译时间)。然后你可以重新使用它,但它需要除了向量之外的东西来保存元素(可能是一个包含所有元素作为成员的结构)。
另外,你真的认为虚拟调度是一个瓶颈吗? 我个人非常怀疑它。
答案 3 :(得分:3)
您将遇到的问题是WidgetCollection::widgets
。向量只能包含一种类型的项目,并且使用CRTP要求每个AbstractWidget
具有不同的类型,并通过所需的派生类型进行模板化。也就是说,你AbstractWidget
看起来像这样:
template< class Derived >
class AbstractWidget {
...
void foo() {
static_cast< Derived* >( this )->foo_impl();
}
...
}
这意味着具有不同AbstractWidget
类型的每个Derived
将构成不同的类型AbstractWidget< Derived >
。将这些存储在一个向量中是行不通的。所以看起来,在这种情况下,虚拟功能是可行的方法。
答案 4 :(得分:3)
如果您需要它们的矢量,则不会。 STL容器是完全同构的,这意味着如果您需要将widgetA和widgetB存储在同一容器中,则它们必须从公共父级继承。并且,如果widgetA :: bar()执行与widgetB :: bar()不同的操作,则必须将这些函数设置为虚拟。
是否所有小部件都需要位于同一个容器中?你可以做点什么
vector<widgetA> widget_a_collection;
vector<widgetB> widget_b_collection;
然后这些功能不需要是虚拟的。
答案 5 :(得分:1)
可能的是,在您完成所有这些努力之后,您将看不到任何性能差异。
这绝对是错误的优化方式。您不会通过更改随机代码行来修复逻辑错误吗?不,那太傻了。在第一次找到实际导致问题的行之前,您不会“修复”代码。那么为什么你会以不同的方式处理性能错误呢?
您需要分析您的应用程序并找出真正的瓶颈所在。然后加速该代码并重新运行探查器。重复直到性能错误(执行速度太慢)消失。