内联 虚函数 和 CRTP
虚函数 和 内联
出于一些系统设计的原因,导致我们在编写程序的时候通常是面向抽象编程的。比方说我们设计了一个计算框架,通常是面向一个接口编程的。
class Expression {
public:
virtual void update(Context* context,DataType &input) = 0;
};
然后我们通常会把逻辑写在具体的实现中,例如一个计算sum聚合的算子:
class SumExpression: public Expression {
public:
void update(Context* context,DataType &input) {
(*(DataType*)context) += input;
}
};
我们在实际使用的时候大多数情况会通过虚函数的方式来调用SumExpression
,从程序结构上看没有什么问题,但是这段程序在执行update的时候会进行一次虚函数调用效率不高。
// virtual function call
DataBlock input_block;
Expression *expression = new SumExpression();
Context context;
for(int i = 0;i < input_block.size();++i) {
expression->update(&context, input_block[i]); // update
}
我们再编写一段非虚函数调用,做一个benchmark
class SumExpressionNoVirtualCall {
public:
void update(Context* context,DataType &input) {
(*(DataType*)context) += input;
}
};
测试代码和测试结果在这里: https://quick-bench.com/q/LOSGHOhXu_n5Y7FpEmQ7PyLKvWw
其中虚函数调用比非虚函数调用的版本满了大约49倍,虚函数调用版本主要的cpu开销在函数调用以及调用前的寄存器状态保存,而非虚函数通过内联优化消除了函数调用。
我们希望SumExpress
跑的更快,因此第二个版本是把基类强制转为超类,再进行函数调用
// 改动 1
for(int i = 0;i < block.size();++i) {
((SumExpression*)(expression))->update(&context, block[i]); // update
}
但是改动1并不会有任何的性能提升。原因是SumExpression
中的update函数仍然是一个虚函数,编译器并不能确定是否有其他类可能会继承SumExpression
重写了update函数,所以编译器不敢轻易的展开。所以我们需要给这个函数加一个关键字final
。
class SumExpressionV2 final: public Expression
重新测试: https://quick-bench.com/q/rzKzsSTRTzDJK7BIakYAu_4Au-c
显然已经和非内联函数性能一致,汇编中也找不到call的痕迹了。
CRTP
我们希望cast代码可以自动的帮我们生成,每次都写太费劲了,假如要写100个算子,编写代价是非常高的,于是神奇的模板就出来了。但是更神奇的是,C++支持递归模板,正好可以帮我们解决这个问题。
class IBase {
public:
virtual void batch_update(Context* context,DataBlock &input_block) = 0;
}
template <typename T>
class Base: public IBase
{
public:
void batch_update(Context* context,DataBlock &input_block)
{
T& derived = static_cast<T&>(*this);
for(int i = 0;i < input_block.size(); ++i) {
derived->update(&context, input_block[i]);
}
}
};
// 这里递归了
class SumExpressionCRTP final: public Base<SumExpressionCRTP> {
public:
void update(Context* context,DataType &input) {
(*(DataType*)context) += input;
}
};
这里虽然batch_update这个仍然是一个虚函数调用,但是调用频率已经变得非常低了。因此借助CRTP,我们可以获得一个易用性和性能都不错的一个版本。