我可以在这里使用奇怪的重复模板模式(C++)吗?
-
05-07-2019 - |
题
我有一个 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
(其中有几十个)通常不会覆盖许多虚拟函数。被覆盖的那些通常具有非常快的实现。
鉴于此,我觉得我可以通过一些巧妙的元编程来优化我的系统。目标是利用函数内联并避免虚拟函数调用,同时保持代码的可管理性。我研究了奇怪的重复模板模式(参见 这里 用于描述)。这似乎 几乎 做我想做的事,但不完全是。
有什么办法可以让 CRTP 在这里为我工作吗?或者,还有其他人能想到的聪明的解决方案吗?
解决方案
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。
此解决方案与运行时多态性相同,只是它要求您维护静态列表<!> quot; virtual <!> quot;方法和已知的子类(不是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
}
}
}
};
其他提示
模拟动态绑定(CRTP 还有其他用途)适用于 基类 认为自己是多态的,但是 客户 实际上只关心一个特定的派生类。例如,您可能拥有代表某些特定于平台的功能的接口的类,并且任何给定的平台只需要一个实现。该模式的要点是将基类模板化,以便即使有多个派生类,基类也可以在编译时知道正在使用哪一个。
当您真正需要运行时多态性时,例如当您有一个容器时,它对您没有帮助 AbstractWidget*
, ,每个元素可以是多个派生类之一,并且您必须迭代它们。在 CRTP(或任何模板代码)中, base<derived1>
和 base<derived2>
是不相关的类。因此也是如此 derived1
和 derived2
. 。它们之间不存在动态多态性,除非它们有另一个公共基类,但随后您又回到了虚拟调用开始的地方。
通过将向量替换为多个向量,您可能会获得一些加速:一个用于您了解的每个派生类,一个通用类用于稍后添加新的派生类并且不更新容器时。然后addWidget做了一些(慢) typeid
检查或对小部件的虚拟调用,将小部件添加到正确的容器,并且当调用者知道运行时类时可能会有一些重载。注意不要意外添加子类 WidgetIKnowAbout
到 WidgetIKnowAbout*
向量。 fooAll
和 barAll
可以循环遍历每个容器,依次(快速)调用非虚拟容器 fooImpl
和 barImpl
然后将被内联的函数。然后他们循环希望更小的 AbstractWidget*
向量,称为虚拟 foo
或者 bar
功能。
这有点混乱,而且不是纯粹的面向对象,但如果几乎所有小部件都属于容器知道的类,那么您可能会看到性能的提高。
请注意,如果大多数小部件属于您的容器不可能知道的类(例如,因为它们位于不同的库中),那么您不可能进行内联(除非您的动态链接器可以内联)。我的不行)。您可以通过修改成员函数指针来降低虚拟调用开销,但收益几乎可以肯定是可以忽略不计的,甚至是负的。虚拟调用的大部分开销都在调用本身,而不是虚拟查找,并且通过函数指针的调用不会被内联。
从另一个角度来看:如果要内联代码,这意味着不同类型的实际机器代码必须不同。这意味着您需要多个循环,或者一个带有开关的循环,因为根据从集合中拉出的某个指针的类型,机器代码显然无法在每次循环时在 ROM 中进行更改。
好吧,我想也许该对象可能包含一些 asm 代码,循环将其复制到 RAM 中,标记可执行文件,然后跳转到其中。但这不是 C++ 成员函数。而且它不能便携地完成。由于复制和 icache 失效,它甚至可能不会很快。这就是虚拟通话存在的原因......
简短的回答是否定的。
答案很长(或者仍然是其他一些答案: - )
您正在动态地尝试确定在运行时要执行的函数(即虚函数是什么)。如果你有一个向量(由于在编译时无法确定成员),那么无论你尝试什么,你都无法弄清楚如何内联函数。
唯一的问题是,如果向量总是包含相同的元素(即,您可以计算出将在运行时执行的编译时间)。然后你可以重新使用它,但它需要除了向量之外的东西来保存元素(可能是一个包含所有元素作为成员的结构)。
另外,你真的认为虚拟调度是一个瓶颈吗?
我个人非常怀疑它。
您将遇到的问题是WidgetCollection::widgets
。向量只能包含一种类型的项目,并且使用CRTP要求每个AbstractWidget
具有不同的类型,并通过所需的派生类型进行模板化。也就是说,你Derived
看起来像这样:
template< class Derived >
class AbstractWidget {
...
void foo() {
static_cast< Derived* >( this )->foo_impl();
}
...
}
这意味着每个AbstractWidget< Derived >
具有不同的<=>类型将构成不同的类型<=>。将这些存储在一个向量中是行不通的。所以看起来,在这种情况下,虚拟功能是可行的方法。
如果您需要它们的矢量,则不会。 STL容器是完全同构的,这意味着如果您需要将widgetA和widgetB存储在同一容器中,则它们必须从公共父级继承。并且,如果widgetA :: bar()执行与widgetB :: bar()不同的操作,则必须将这些函数设置为虚拟。
是否所有小部件都需要位于同一个容器中?你可以做点什么
vector<widgetA> widget_a_collection;
vector<widgetB> widget_b_collection;
然后这些功能不需要是虚拟的。
可能的是,在您完成所有这些努力之后,您将看不到任何性能差异。
这绝对是错误的优化方式。您不会通过更改随机代码行来修复逻辑错误吗?不,那太傻了。你没有<!> quot; fix <!> quot;代码,直到您第一次找到哪些行实际导致您的问题。那么为什么你会以不同的方式处理性能错误呢?
您需要分析您的应用程序并找出真正的瓶颈所在。然后加速该代码并重新运行探查器。重复直到性能错误(执行速度太慢)消失。