C++ 代表团的极速实现






4.96/5 (62投票s)
一个代表库的实现,它比“最快的 C++ 代表团”更快,并且与 C++ 标准完全兼容。
引言
这是对文章“Don Clugston 的成员函数指针和最快的 C++ 代表团”的回应。Don 提出了一种代表团(后称 FastDelegate
)的方法,它在最简单的情况下需要与通过成员函数指针调用相同的调用代码(他解释了为什么某些编译器会为多态类和具有虚拟继承的类生成更复杂的代码)。他解释了为什么许多其他流行的方法效率低下。不幸的是,他的方法基于“一个可怕的黑客”(正如他所说)。它适用于许多流行的编译器,但与 C++ 标准不兼容。
FastDelegate
似乎是最快的方法。但我认为这种说法需要证明,因为现代 C++ 优化编译器可以做出令人难以置信的事情。我相信 boost::function
和其他基于动态内存分配的代表团很慢,但谁说没有其他好的方法呢?
我将提出另一种方法,它
- 速度快
- 不使用动态分配的内存
- 与 C++ 标准完全兼容
另一种代表团方法
让我们考虑一个接收一个参数且不返回值的代表团。它可以使用首选语法(如 boost::function
和 FastDelegate
,我的库支持首选和兼容语法;详见文档)按以下方式定义
delegate<void (int)>
我简化了它的代码以帮助您理解它的工作原理。以下代码是通过删除所考虑代码上方和下方的不必要行,并用具体类型替换模板参数而得出的。
class delegate
{
public:
delegate()
: object_ptr(0)
, stub_ptr(0)
{}
template <class T, void (T::*TMethod)(int)>
static delegate from_method(T* object_ptr)
{
delegate d;
d.object_ptr = object_ptr;
d.stub_ptr = &method_stub<T, TMethod>; // #1
return d;
}
void operator()(int a1) const
{
return (*stub_ptr)(object_ptr, a1);
}
private:
typedef void (*stub_type)(void* object_ptr, int);
void* object_ptr;
stub_type stub_ptr;
template <class T, void (T::*TMethod)(int)>
static void method_stub(void* object_ptr, int a1)
{
T* p = static_cast<T*>(object_ptr);
return (p->*TMethod)(a1); // #2
}
};
因此,一个委托由一个非类型化数据指针(因为委托不能依赖于接收器的类型)和一个函数指针组成。这个函数接收数据指针作为额外参数。它将数据指针转换为对象指针(“void*
”,与成员指针不同,可以安全地转换回对象指针:[expr.static.cast],第10项),并调用所需的成员函数。
当你创建一个非空委托时,你通过获取它的地址隐式实例化了一个 stub
函数(参见上面第 #1 行)。这是可能的,因为 C++ 标准允许使用指向成员的指针或指向函数的指针作为模板参数([temp.params
],第 4 项)。
SomeObject obj;
delegate d = delegate::from_member<SomeObject,
&SomeObject::someMethod>(&obj);
现在,'d
' 包含一个在编译时绑定到 'someMethod
' 的 stub
函数指针。尽管指定了成员指针,但第 2 行的调用与直接方法调用一样快(因为其值在编译时已知)。
通常,委托可以通过内联函数调用运算符调用,该运算符通过 stub
函数将调用重定向到目标方法
d(10); // invocation of SomeObject::someMethod
// for obj and passing them 10 as a parameter
当然,这假设了一个额外的函数调用,但开销主要取决于优化器。实际上,可能根本没有开销。
性能测量
我用各种虚拟/非虚拟方法组合、不同数量的参数和不同类型的继承测量了委托调用的性能。我还测量了绑定到函数和 static
方法的委托的性能。我使用 MS Visual C++ 7.1 和 Intel C++ 8.0 编译器在 P4 Celeron 处理器上比较了 FastDelegate
和我的方法的性能。
在复杂情况下,使用 stub
函数可能会导致明显的开销(在 MSVC 上高达 5.5 倍,在 Intel 上高达 2.4 倍)。但有时“最快可能的委托”会更慢(在 Intel 上高达 15%,在 MSVC 上略慢)。它们在 static
成员和自由函数上总是更慢。这是怎么回事?
在反汇编代码分析过程中,我发现了一个有趣的现象。在最坏的情况下,编译器会复制 stub
函数的所有参数并将它们传递给目标方法。在某些情况下(如果目标方法没有参数或者转换是简单的),优化器会将 stub
函数简化为一条简单的跳转指令。当目标方法可以内联时,优化器会将其代码放入 stub
函数中。在这种情况下,完全没有开销。
“最快可能的委托”被迫使用 'thiscall
' 调用约定。我的委托可以自由使用任何调用约定(除了 'thiscall
'),包括 '__fastcall
'。它允许通过寄存器传递最多两个 int
大小的参数('thiscall
' 只通过 ECX
传递 'this
' 指针)。
实际上,有一种简单的方法可以让你的基于委托的代码变得极其快速(如果你真的需要它)
- 不要使用复杂对象作为参数类型和返回值(改用引用),
- 不要使用虚方法作为委托的目标(因为通常它可能无法内联),
- 将目标方法实现和委托创建代码放在同一个编译单元中。
您可以尝试使用我的基准代码来衡量您平台和编译器的性能。
复制和比较委托
对于两种类型的委托(与基于动态内存分配的委托,如 boost::function
不同),复制构造函数的性能都不是问题。然而,我的委托可以复制得更快一些,因为它们倾向于占用更少的空间。
我的委托无法比较。因为委托不包含指向方法的指针,所以没有定义比较运算符。指向 stub
函数的指针在不同的编译单元中可能不同。实际上,这个功能缺失是 Don Clugston 对我的方法不满的主要原因。
然而,我认为比较指针到方法的可能性是危险的。它可能运行良好,直到你某次将某个类设置为可内联。
我只知道一个你需要比较委托的原因。那就是像 C# 那样的事件语法。它看起来很棒,但没有动态内存分配就无法实现。此外,在 C++ 中,它在某些情况下可能无法正常工作。我想建议另一种事件传播机制,在我看来更适合 C++。
可移植性
尽管这种方法与 C++ 标准兼容,但遗憾的是它在某些编译器上不起作用。我未能成功在 Borland C++ 上编译测试代码。在 MSVC 7.1 上,首选语法不起作用,尽管它成功编译了相同语法的 boost::function
。
我认为这是因为使用了不常用的语言特性。
事件库
我正在提出一个事件库,以证明委托并不真正需要比较操作。实际上,这个事件库与我的委托并不紧密相关。它可以使用多种委托,包括 boost::function
。此外,它还可以使用回调接口(如 Java 中的那些)。
我的事件库提供了一种快速订阅和取消订阅事件生产者的方法(甚至在事件发出期间),并且也不使用动态分配的内存(如果您对快速委托感兴趣,这应该对您很重要)。
这个库提供了两个实体:event_source
(它是 boost::signal
的简化模拟)和 event_binder
(boost::signals::scoped_connection
的模拟)。通常,事件生产者保留 event_source
,事件消费者保留 event_binder
。当 event_source
和 event_binder
都存在时,生产者和消费者之间才存在连接。
您不能使用匿名连接。实际上在 Boost 中,您可以通过两种方式使用它
- 你完全确定事件消费者比事件生产者存在的时间更长,并且
- 您应该使用
boost::signals::trackable
作为事件消费者的基类(在我的库中可以实现类似的,但我不确定这是个好主意)。
您可以在 C# 风格的多播委托中使用它,但还有另一个问题:您必须维护成对的操作(订阅和取消订阅),但它们的正确性无法在编译时检查。
更多详情,请参见文档。
结论
也许 C++ 设计的一些细节并不理想,但我看不到任何理由去打破 C++ 标准。此外,有时黑客行为并不能让优化器发挥其全部能力。
参考文献
历史
- 2005年7月18日:初始版本